当前位置: 首页 > news >正文

【实战】Spring Security Oauth2自定义授权模式接入手机验证

文章目录

    • 前言
    • 技术积累
      • Oauth2简介
      • Oauth2的四种模式
        • 授权码模式
        • 简化模式
        • 密码模式
        • 客户端模式
        • 自定义模式
    • 实战演示
      • 1、mavan依赖引入
      • 2、自定义手机用户
      • 3、自定义手机用户信息获取服务
      • 4、自定义认证令牌
      • 5、自定义授权模式
      • 6、自定义实际认证提供者
      • 7、认证服务配置
      • 8、Oauth2配置
      • 9、资源服务配置
    • 测试用例
      • 1、创建测试请求
      • 2、测试用例预演
      • 3、测试结果
    • 写在最后

前言

最近在修改一个之前使用Oauth2的旧项目,该项目使用了原始的账号密码登录。现在有一个需求是在当前的基础上增加手机验证码登录,让系统可以同时支持账密、手机验证码登录两种方式。哈哈,看到这个需求心里就想着可以参照Oauth2的账密验证逻辑进行改造,比如自定义认证对象、自定义授权模式、自定义实际授权者,最后将上面几个添加到Oauth2里面让其能够识别即可。

在这里插入图片描述

技术积累

Oauth2简介

OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源 (如头像、照片、视频等),并且在这个过程中无须将用户名和密码提供给第三方应用。通过令牌 (token) 可以实现这一功能。每一个令牌授权一个特定的网站在特定的时间段内允许可访问特定的资源。

OAuth 让用户可以授权第三方网站灵活访问它们存储在另外一些资源服务器上的特定信息,而非所有的内容。对于用户而言,我们在互联网应用中最常见的 OAuth 应用就是各种第三方登录,例如 QQ授权登录、微信授权登录、微博授权登录、GitHub 授权登录等。

Oauth2的四种模式

授权码模式

大概就是客户端需要拿到一个预授权码,然后通过预授权码换取真实的token,最后使用token进行访问。比如我们的微信授权登录、飞书授权登录等等。
在这里插入图片描述

简化模式

简化模式是授权码模式简化版本,客户端不用先申请预授权码,直接通过具体的参数拿到token,最后访问资源。比如我们微信appId、appSecrt拿到access_token。
在这里插入图片描述

密码模式

这个就是非常原始的账户、密码登录,客户端携带账户和密码直接拿到token,最后使用token进行资源访问。比如我们现在常规系统的账密登录。
在这里插入图片描述

客户端模式

这种直接客户端向授权服务注册,直接拿到token,比如会员客户端等等。

在这里插入图片描述

自定义模式

自定义模式就是我们今天的重点,我们不用Oauth2的四种模式,直接用自己的方式实现一个手机验证码登录功能。

大概得流程就是:
1、自定义认证令牌
extends AbstractAuthenticationToken
自由实现未认证和已认证的构造令牌的方法

2、自定义授权模式
extends AbstractTokenGranter
在授权模式中我们会获取到认证路径中的手机号和验证码,并产生一个未认证的认证令牌交给Oauth2

3、自定义实际认证提供者
implements AuthenticationProvider
在实际的认证者中我们会拿到在授权模式中提供的手机号和验证码,然后进行业务比对,比如是否存在、是否过期等等。如果全部验证通过会产生一个完成认证的认证令牌交给Oauth2

4、授权配置
extends AuthorizationServerConfigurerAdapter
这个里面需要配置 客户端
ClientDetailsServiceConfigurer
以及将自定义和默认授权模式交给Oauth2 AuthorizationServerEndpointsConfigurer

5、Oauth2配置
extends WebSecurityConfigurerAdapter
配置哪些请求需要进行拦截和免登录
configure(HttpSecurity http)
配置密码模式的用户名和密码,并将自定义认证提供者加入
configure(AuthenticationManagerBuilder auth)

6、资源服务器配置
extends ResourceServerConfigurerAdapter
定义哪些资源需要被拦截和放行
configure(HttpSecurity http)
如果是生成环境需要配置token策略和服务端保持一致。

实战演示

本次演示是将资源服务和客户服务放置在同一个项目上,所有采用的内存报错token。如果使用在生产环境请修改为从数据库拿去用户和客户信息,以及token持久化。

1、mavan依赖引入

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup parent from repository -->
</parent>
<properties><java.version>8</java.version><spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
<!--安全模块-->
<dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

2、自定义手机用户

PhoneUser

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;/*** PhoneUser* @author senfel* @version 1.0* @date 2024/8/7 18:34*/
public class PhoneUser extends User {@Getterprivate Integer id;@Getterprivate String name;@Getterprivate String phone;public PhoneUser(Integer id,String name,String phone,String username, String password, Collection<? extends GrantedAuthority> authorities) {super(username, password, authorities);this.id = id;this.name = name;this.phone = phone;}
}

3、自定义手机用户信息获取服务

PhoneUserDetailsService

import cn.hutool.core.util.ArrayUtil;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;/*** PhoneUserDetailsService* 手机用户信息获取服务* @author senfel* @version 1.0* @date 2024/8/7 18:28*/
@Component
public class PhoneUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {System.err.println("PhoneUserDetailsService获取到的phone:"+phone);//TODO 从数据库获取手机用户数据,默认admin_role权限集String[] permissionArr = new String[]{"admin_role"};return new PhoneUser(1,"senfel-test-phone","18788888888","18788888888","", initAuthority(permissionArr));}private Collection<? extends GrantedAuthority> initAuthority(String[] permissionArr) {Set<String> dbAuthsSet = new HashSet<>();if (ArrayUtil.isNotEmpty(permissionArr)) {dbAuthsSet.addAll(Arrays.asList(permissionArr));}return AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(new String[0]));}
}

4、自定义认证令牌

PhoneAuthenticationToken

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;/*** PhoneAuthenticationToken* 手机验证码认证令牌* @author senfel* @version 1.0* @date 2024/8/7 17:33*/
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = 530L;//表示认证主体,通常是用户对象(UserDetails),这里传入手机号private final Object principal;//存储了与主体关联的认证信息,例如密码,这里传入验证码private Object credentials;/*** 自定义已未证对象* @param principal* @param credentials* @author senfel* @date 2024/8/7 17:39* @return*/public PhoneAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal;this.credentials = credentials;setAuthenticated(false);}/*** 自定义已认证对象* @param authorities 表示主体所拥有的权限集合* @param principal* @param credentials* @author senfel* @date 2024/8/7 17:39* @return*/public PhoneAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {super(authorities);this.principal = principal;this.credentials = credentials;//是否认证标识setAuthenticated(true);}@Overridepublic Object getCredentials() {return this.credentials;}@Overridepublic Object getPrincipal() {return this.principal;}
}

5、自定义授权模式

PhoneCodeTokenGranter

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import java.util.LinkedHashMap;
import java.util.Map;/*** PhoneCodeTokenGranter* 手机验证码授权模式* @author senfel* @version 1.0* @date 2024/8/7 17:43*/
public class PhoneCodeTokenGranter extends AbstractTokenGranter {//授权类型名称private static final String GRANT_TYPE = "phonecode";private final AuthenticationManager authenticationManager;/*** 手机验证码授权模式构造函数* @param tokenServices* @param clientDetailsService* @param requestFactory* @param authenticationManager* @author senfel* @date 2024/8/7 18:06* @return*/public PhoneCodeTokenGranter( AuthenticationManager authenticationManager,AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {this(authenticationManager,tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);}public PhoneCodeTokenGranter(AuthenticationManager authenticationManager,AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {super(tokenServices, clientDetailsService, requestFactory, grantType);this.authenticationManager = authenticationManager;}@Overrideprotected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());//获取参数String phone = parameters.get("phone");String phonecode = parameters.get("phonecode");//创建未认证对象Authentication userAuth = new PhoneAuthenticationToken(phone, phonecode);((AbstractAuthenticationToken) userAuth).setDetails(parameters);try {//进行身份认证userAuth = authenticationManager.authenticate(userAuth);} catch (AccountStatusException ase) {//将过期、锁定、禁用的异常统一转换throw new InvalidGrantException(ase.getMessage());} catch (BadCredentialsException e) {// 验证码错误,我们应该发送400/invalid grantthrow new InvalidGrantException(e.getMessage());}if (userAuth == null || !userAuth.isAuthenticated()) {throw new InvalidGrantException("用户认证失败: " + phone);}OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);return new OAuth2Authentication(storedOAuth2Request, userAuth);}}

6、自定义实际认证提供者

PhoneAuthenticationProvider

import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;/*** PhoneAuthenticationProvider* 手机验证码实际认证供应者* @author senfel* @version 1.0* @date 2024/8/7 18:12*/
@Setter
public class PhoneAuthenticationProvider implements AuthenticationProvider {private StringRedisTemplate redisTemplate;private PhoneUserDetailsService phoneUserDetailsService;public static final String PHONE_CODE_SUFFIX = "phone:code:";@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {//先将authentication转为我们自定义的Authentication对象PhoneAuthenticationToken authenticationToken = (PhoneAuthenticationToken) authentication;//校验参数Object principal = authentication.getPrincipal();Object credentials = authentication.getCredentials();if (principal == null || "".equals(principal.toString()) || credentials == null || "".equals(credentials.toString())){throw new InternalAuthenticationServiceException("手机/手机验证码为空!");}//获取手机号和验证码String phone = (String) authenticationToken.getPrincipal();String code = (String) authenticationToken.getCredentials();//查找手机用户信息,验证用户是否存在UserDetails userDetails = phoneUserDetailsService.loadUserByUsername(phone);if (userDetails == null){throw new InternalAuthenticationServiceException("用户手机不存在!");}String codeKey =  PHONE_CODE_SUFFIX+phone;//手机用户存在,验证手机验证码是否正确if (!redisTemplate.hasKey(codeKey)){throw new InternalAuthenticationServiceException("验证码不存在或已失效!");}String realCode = redisTemplate.opsForValue().get(codeKey);if (StringUtils.isBlank(realCode) || !realCode.equals(code)){throw new InternalAuthenticationServiceException("验证码错误!");}//返回认证成功的对象PhoneAuthenticationToken phoneAuthenticationToken = new PhoneAuthenticationToken(userDetails.getAuthorities(),phone,code);//details是一个泛型属性,用于存储关于认证令牌的额外信息。其类型是 Object,所以你可以存储任何类型的数据。这个属性通常用于存储与认证相关的详细信息,比如用户的角色、IP地址、时间戳等。phoneAuthenticationToken.setDetails(userDetails);return phoneAuthenticationToken;}/*** ProviderManager 选择具体Provider时根据此方法判断* 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口*/@Overridepublic boolean supports(Class<?> authentication) {//isAssignableFrom方法如果比较类和被比较类类型相同,或者是其子类、实现类,返回truereturn PhoneAuthenticationToken.class.isAssignableFrom(authentication);}}

7、认证服务配置

AuthorizationServerConfig

import com.example.ccedemo.security.PhoneCodeTokenGranter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import java.util.ArrayList;
import java.util.List;/*** AuthorizationServerConfig* 认证服务配置* @author senfel* @version 1.0* @date 2024/8/7 19:22*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;/*** 配置允许访问系统的客户端* @param clients* @author senfel* @date 2024/8/8 14:17* @return void*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//clients.jdbc()生产环境从数据库加载//模拟内置一个系统客户端clients.inMemory().withClient("admin").secret("{bcrypt}"+new BCryptPasswordEncoder().encode("12315")).scopes("server").authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token","phonecode");}/*** 断点配置* @param endpoints* @author senfel* @date 2024/8/8 14:17* @return void*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//tokenGranterendpoints.tokenGranter(new CompositeTokenGranter(initGranters(endpoints)));}/*** 加入所有自定义的和原有的认证模式配置* @param endpoints* @author senfel* @date 2024/8/9 9:33* @return java.util.List<org.springframework.security.oauth2.provider.TokenGranter>*/private List<TokenGranter> initGranters(AuthorizationServerEndpointsConfigurer endpoints) {//TODO 如果资源服务与授权服务分开,则需要token持久化,这里默认使用内存存储AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();ClientDetailsService clientDetailsService = endpoints.getClientDetailsService();OAuth2RequestFactory oAuth2RequestFactory = endpoints.getOAuth2RequestFactory();AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();//自定义GranterList<TokenGranter> customTokenGranters = new ArrayList<>();customTokenGranters.add(new PhoneCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));//添加密码模式customTokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));//刷新模式customTokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));//简易模式customTokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));//客户端模式customTokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));//授权码模式customTokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, oAuth2RequestFactory));return customTokenGranters;}}

8、Oauth2配置

OAuth2SecurityConfig

import com.example.ccedemo.security.PhoneAuthenticationProvider;
import com.example.ccedemo.security.PhoneUserDetailsService;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;/*** OAuth2SecurityConfig* @author senfel* @version 1.0* @date 2024/8/7 19:08*/
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate PhoneUserDetailsService phoneUserDetailsService;@Override@SneakyThrowsprotected void configure(HttpSecurity http) {http.csrf().disable()// 关闭csrf.authorizeRequests().anyRequest().authenticated();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic PasswordEncoder passwordEncoder() {return PasswordEncoderFactories.createDelegatingPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//auth.jdbcAuthentication()生产环境从数据库加载//模拟验证用户名密码登录auth.inMemoryAuthentication().withUser("admin").password("{bcrypt}"+new BCryptPasswordEncoder().encode("123456"))//设置权限.authorities("admin_role").and().withUser("user").password("{bcrypt}"+new BCryptPasswordEncoder().encode("123456"))//设置权限.authorities("user_role");//放入自定义的认证提供者auth.authenticationProvider(phoneAuthenticationProvider());}/*** 手机验证码登录的认证提供者* @author senfel* @date 2024/8/9 9:34* @return com.example.ccedemo.security.PhoneAuthenticationProvider*/@Beanpublic PhoneAuthenticationProvider phoneAuthenticationProvider(){//实例化provider,把需要的属性set进去PhoneAuthenticationProvider phoneAuthenticationProvider = new PhoneAuthenticationProvider();phoneAuthenticationProvider.setRedisTemplate(redisTemplate);phoneAuthenticationProvider.setPhoneUserDetailsService(phoneUserDetailsService);return phoneAuthenticationProvider;}
}

9、资源服务配置

ResourceServerConfig

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;/*** ResourceServerConfig* 资源服务配置* @author senfel* @version 1.0* @date 2024/8/9 9:35*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {/*** 配置security的安全机制* TODO 如果资源服务与授权服务分开,则需要token持久化* @author senfel* @date 2024/8/9 9:56* @return void*/@Overridepublic void configure(HttpSecurity http) throws Exception {//#oauth2.hasScope()校验客户端的权限,这个server是在客户端中的scopehttp.authorizeRequests()// 免认证的请求.antMatchers("/oauth2-test/get-user-name").permitAll().antMatchers("/**").access("#oauth2.hasScope('server')").anyRequest().authenticated();}
}

测试用例

1、创建测试请求

Oauth2Controller

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** Oauth2Controller* @author senfel* @version 1.0* @date 2024/8/8 10:43*/
@RestController
@RequestMapping("/oauth2-test")
public class Oauth2Controller {/*** 不用鉴权就可以获取用户名称* @author senfel* @date 2024/8/8 17:53* @return java.lang.String*/@RequestMapping("/get-user-name")public String getUserName() {return "senfel";}/*** 必须admin_role权限才能获取密码* @author senfel* @date 2024/8/8 17:53* @return java.lang.String*/@PreAuthorize("hasAnyAuthority('admin_role')")@RequestMapping("/get-user-password")public String getUserPassword() {return "123321123467";}/*** 有登录权限才能获取年龄* @author senfel* @date 2024/8/8 17:53* @return java.lang.String*/@RequestMapping("/get-user-age")public String getUserAge() {return "18";}
}

2、测试用例预演

我们在上面创建了三个测试请求:
2.1 在资源服务配置中我们将get-user-name路径直接放行,也就是不需要认证也能够访问;
2.2 在Oauth2的配置中我们模拟了两个用户,admin拥有dmin_role权限,user拥有user_role权限。并且我们在get-user-password资源上添加了 @PreAuthorize(“hasAnyAuthority(‘admin_role’)”)仅让有admin_role权限的用户才能访问,那么这里可以让admin用户认证后访问,user登录后访问拒绝;
2.3 在资源服务配置我们让除去name的其他资源都必须认证,则get-user-age资源在所有用户认证成功后都能够访问。

3、测试结果

3.1 不登录进行验证
由于资源配置放行则不登录也可以访问
在这里插入图片描述

3.2 密码模式
3.2.1 调用 /oauth/token登录
使用basic admin 12315客户端验证
admin登录测试

admin登录
在这里插入图片描述
登录后可用访问age资源:
在这里插入图片描述
由于admin有admin_role权限,登录后可用访问password资源:
在这里插入图片描述

user登录测试

user登录
在这里插入图片描述
登录后可用访问age资源:
在这里插入图片描述
由于user只有user_role权限,登录后拒绝访问password资源:

3.3 自定义手机验证登录

手机验证登录
在这里插入图片描述
登录后可访问age资源:
在这里插入图片描述
由于当前phone拥有admin_role权限,登录后可访问password资源:
在这里插入图片描述

写在最后

Oauth2自定义认证模式还是比较简单,直接自定义认证令牌、自定义授权模式、自定义实际认证者、然后将自定义的授权模式和认证者交给Oauth2。最后,我们在资源配置中可以配置受限资源和免登录资源,以及token储存方式、用户加载方式等等即可。

相关文章:

【实战】Spring Security Oauth2自定义授权模式接入手机验证

文章目录 前言技术积累Oauth2简介Oauth2的四种模式授权码模式简化模式密码模式客户端模式自定义模式 实战演示1、mavan依赖引入2、自定义手机用户3、自定义手机用户信息获取服务4、自定义认证令牌5、自定义授权模式6、自定义实际认证提供者7、认证服务配置8、Oauth2配置9、资源…...

Redis数据失效监听

一、配置Redis开启 打开conf/redis.conf 文件&#xff0c;添加参数&#xff1a;notify-keyspace-events Ex 二、验证配置 步骤一&#xff1a;进入redis客户端&#xff1a;redis-cli步骤二&#xff1a;执行 CONFIG GET notify-keyspace-events &#xff0c;如果有返回值证明配…...

【达梦数据库】-SQL调优思路

【达梦数据库】-SQL调优思路 --查看统计信息是否准确 select table_name,num_rows,blocks,last_analyzed from user_tables where table_name表名; #默认每周六1点进行全库信息统计1、确认SQL --sql select * from test;2、查看ET ---------------------------------------…...

DispatcherServlet 源码分析

一.DispatcherServlet 源码分析 本文仅了解源码内容即可。 1.观察我们的服务启动⽇志: 当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.所有请求都会先进到DispatcherServlet&#xff0c;执⾏doDispatch 调度⽅法. 如果有拦截器, 会先执⾏拦截器…...

代码随想录算法训练营第十八天| 530.二叉搜索树的最小绝对差 ● 501.二叉搜索树中的众数 ● 236. 二叉树的最近公共祖先

题目&#xff1a; 530. 二叉搜索树的最小绝对差 给你一个二叉搜索树的根节点 root &#xff0c;返回 树中任意两不同节点值之间的最小差值 。 差值是一个正数&#xff0c;其数值等于两值之差的绝对值。 示例 1&#xff1a; 输入&#xff1a;root [4,2,6,1,3] 输出&#xff1a;…...

会议室占用的时间(75%用例)D卷(JavaPythonC++Node.jsC语言)

现有若干个会议,所有会议共享--个会议室,用数组表示各个会议的开始时间和结束时间,格式为: 会议1开始时间,会议1结束时间 会议2开始时间,会议2结束时间 请计算会议室占用时间段。 输入描述: 第一行输入一个整数 n,表示会议数量 之后输入n行,每行两个整数,以空格分隔,…...

C++初阶_1:namespace

本章详细解说&#xff1a;namespace 。 namespace&#xff1a; namespace,意为&#xff1a;命名空间&#xff0c;c的关键字&#xff08;关键字&#xff0c;就是提示&#xff1a;取变量名&#xff0c;函数名时不能与之撞名&#xff09;。 namespace的价值&#xff1a; 为了解…...

低代码开发平台:效率革命还是质量隐忧?

如何看待“低代码”开发平台的兴起&#xff1f; 近年来&#xff0c;“低代码”开发平台如雨后春笋般涌现&#xff0c;承诺让非专业人士也能快速构建应用程序。这种新兴技术正在挑战传统软件开发模式&#xff0c;引发了IT行业的广泛讨论。低代码平台是提高效率的利器&#xff0…...

在 Django 表单中传递自定义表单值到视图

在Django中&#xff0c;我们可以通过表单的初始化参数initial来传递自定义的初始值给表单字段。如果我们想要在视图中设置表单的初始值&#xff0c;可以在视图中创建表单的实例时&#xff0c;传递一个字典给initial参数。 1、问题背景 我们遇到了这样一个问题&#xff1a;在使…...

Android之复制文本(TextView)剪贴板

效果图&#xff1a; 功能简单就是点击“复制”&#xff0c;将邀请码复制到 剪贴板中 布局 <androidx.constraintlayout.widget.ConstraintLayoutandroid:id"id/clCode"android:layout_width"dimen/dp_0"android:layout_height"dimen/dp_49"…...

Ubuntu24.04设置国内镜像软件源

参考文章&#xff1a; Ubuntu24.04更换源地址&#xff08;新版源更换方式&#xff09; - 陌路寒暄 一、禁用原来的软件源 Ubuntu24.04 的源地址配置文件发生改变&#xff0c;不再使用以前的 sources.list 文件&#xff0c;升级 24.04 之后&#xff0c;该文件内容变成了一行注…...

分布式与微服务详解

1. 单机架构 只有一台机器&#xff0c;这个机器负责所有的工作 &#xff08;这里假定一个电商网站&#xff09; 现在大部分公司的产品都是单机架构 。 2. 分布式架构 一台机器的硬件资源是有限的&#xff0c;服务器处理请求是需要占用硬件资源的&#xff0c;如果业务增长&a…...

Vue设置滚动条自动保持到最底端

需求描述&#xff1a;在开发中我们常常会遇到需要让滚动条保持到最底端的需求&#xff0c;比如在开发一个聊天框时&#xff0c;请求接口拿到消息列表数据&#xff0c;展示到前端页面时&#xff0c;需要让滚动条自动滚到最底端&#xff0c;以此来展示最后的聊天记录。同时&#…...

uniapp创建一个新项目并导入uview-plus框架

近年来&#xff0c;随着技术的发展&#xff0c;人们越来越意识到跨平台和统一的重要性。对于同一款应用来说&#xff0c;一般都会有移动端、PC端、甚至小程序端。这是由于设备的不同&#xff0c;我们必须要做很多的客户端来满足不同的用户需求。但是由于硬件设施的不同&#xf…...

LabVIEW光电在线测振系统

开发了一种基于LabVIEW软件和光电技术的在线测振系统。该系统利用激光作为调制光源&#xff0c;并通过位置敏感型光电传感器&#xff08;PSD&#xff09;进行轴振动的实时检测。其主要特点包括非接触式测量、广泛的测量范围、高灵敏度和快速响应时间&#xff0c;且具备优良的抗…...

分布式光伏电站 转化能源 丰富用电结构

分布式光伏系统是一种利用分散式的可再生能源&#xff0c;在靠近用户端的地方安装光伏发电设施&#xff0c;通过光伏效应将太阳能转化为直流电能&#xff0c;并通过逆变器将其转换为交流电&#xff0c;以供用户使用的系统。以下是对分布式光伏系统的详细阐述&#xff1a; 一、…...

环境配置:如何在IntelliJ IDEA中安装和修改JDK版本配置(以Windows为例)

环境配置&#xff1a;如何在IntelliJ IDEA中安装和修改JDK版本配置&#xff08;以Windows为例&#xff09; 为了在Java开发中使用最新的功能和优化&#xff0c;升级和配置JDK版本是必不可少的。本文将详细介绍如何下载、安装、配置最新的JDK版本&#xff0c;并在IntelliJ IDEA…...

Spring AOP 原理——代理模式

目录 一、代理模式 1.1 静态代理 1.2 动态代理 1.2.1 JDK动态代理 1.2.2 CGLIB动态代理 Spring AOP 是基于动态代理来实现AOP的。 一、代理模式 代理模式, 也叫委托模式。该模式是为其他对象提供⼀种代理以控制对这个对象的访问。它的作用就是通过提供一个代理类&#…...

leetcode 234.回文链表

思路&#xff1a;其实就是判断反转链表是不是和原链表一样的问题。 我们可以借助反转链表的思路&#xff0c;首先我们先把链表的全部元素正向存储&#xff0c;然后再把链表进行反转。 之后我们再遍历反转之后的链表结点元素是不是和刚刚存储数组里面的元素一致就可以了。一旦…...

AD中Split Planes 的作用和功能

在 Altium Designer (AD) 中&#xff0c;Split Planes 功能允许你在一个平面层&#xff08;例如电源层或地层&#xff09;上分割出多个不同的区域&#xff0c;每个区域可以分配给不同的网络&#xff08;net&#xff09;。这对于设计中需要管理多种电源或接地类型的情况下非常有…...

Spring Boot 实现流式响应(兼容 2.7.x)

在实际开发中&#xff0c;我们可能会遇到一些流式数据处理的场景&#xff0c;比如接收来自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 内容&#xff0c;并将其原样中转给前端页面或客户端。这种情况下&#xff0c;传统的 RestTemplate 缓存机制会…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例

文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

Mysql8 忘记密码重置,以及问题解决

1.使用免密登录 找到配置MySQL文件&#xff0c;我的文件路径是/etc/mysql/my.cnf&#xff0c;有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

WPF八大法则:告别模态窗口卡顿

⚙️ 核心问题&#xff1a;阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程&#xff0c;导致后续逻辑无法执行&#xff1a; var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题&#xff1a…...

MyBatis中关于缓存的理解

MyBatis缓存 MyBatis系统当中默认定义两级缓存&#xff1a;一级缓存、二级缓存 默认情况下&#xff0c;只有一级缓存开启&#xff08;sqlSession级别的缓存&#xff09;二级缓存需要手动开启配置&#xff0c;需要局域namespace级别的缓存 一级缓存&#xff08;本地缓存&#…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅

目录 前言 操作系统与驱动程序 是什么&#xff0c;为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中&#xff0c;我们在使用电子设备时&#xff0c;我们所输入执行的每一条指令最终大多都会作用到硬件上&#xff0c;比如下载一款软件最终会下载到硬盘上&am…...

git: early EOF

macOS报错&#xff1a; Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/.git/ remote: Enumerating objects: 2691797, done. remote: Counting objects: 100% (1760/1760), done. remote: Compressing objects: 100% (636/636…...

云安全与网络安全:核心区别与协同作用解析

在数字化转型的浪潮中&#xff0c;云安全与网络安全作为信息安全的两大支柱&#xff0c;常被混淆但本质不同。本文将从概念、责任分工、技术手段、威胁类型等维度深入解析两者的差异&#xff0c;并探讨它们的协同作用。 一、核心区别 定义与范围 网络安全&#xff1a;聚焦于保…...

Mysql故障排插与环境优化

前置知识点 最上层是一些客户端和连接服务&#xff0c;包含本 sock 通信和大多数jiyukehuduan/服务端工具实现的TCP/IP通信。主要完成一些简介处理、授权认证、及相关的安全方案等。在该层上引入了线程池的概念&#xff0c;为通过安全认证接入的客户端提供线程。同样在该层上可…...

Python学习(8) ----- Python的类与对象

Python 中的类&#xff08;Class&#xff09;与对象&#xff08;Object&#xff09;是面向对象编程&#xff08;OOP&#xff09;的核心。我们可以通过“类是模板&#xff0c;对象是实例”来理解它们的关系。 &#x1f9f1; 一句话理解&#xff1a; 类就像“图纸”&#xff0c;对…...