当前位置: 首页 > 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;。这对于设计中需要管理多种电源或接地类型的情况下非常有…...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

Unity3D中Gfx.WaitForPresent优化方案

前言 在Unity中&#xff0c;Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染&#xff08;即CPU被阻塞&#xff09;&#xff0c;这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案&#xff1a; 对惹&#xff0c;这里有一个游戏开发交流小组&…...

黑马Mybatis

Mybatis 表现层&#xff1a;页面展示 业务层&#xff1a;逻辑处理 持久层&#xff1a;持久数据化保存 在这里插入图片描述 Mybatis快速入门 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6501c2109c4442118ceb6014725e48e4.png //logback.xml <?xml ver…...

《Playwright:微软的自动化测试工具详解》

Playwright 简介:声明内容来自网络&#xff0c;将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具&#xff0c;支持 Chrome、Firefox、Safari 等主流浏览器&#xff0c;提供多语言 API&#xff08;Python、JavaScript、Java、.NET&#xff09;。它的特点包括&a…...

JVM垃圾回收机制全解析

Java虚拟机&#xff08;JVM&#xff09;中的垃圾收集器&#xff08;Garbage Collector&#xff0c;简称GC&#xff09;是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象&#xff0c;从而释放内存空间&#xff0c;避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习

禁止商业或二改转载&#xff0c;仅供自学使用&#xff0c;侵权必究&#xff0c;如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

JAVA后端开发——多租户

数据隔离是多租户系统中的核心概念&#xff0c;确保一个租户&#xff08;在这个系统中可能是一个公司或一个独立的客户&#xff09;的数据对其他租户是不可见的。在 RuoYi 框架&#xff08;您当前项目所使用的基础框架&#xff09;中&#xff0c;这通常是通过在数据表中增加一个…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合

在汽车智能化的汹涌浪潮中&#xff0c;车辆不再仅仅是传统的交通工具&#xff0c;而是逐步演变为高度智能的移动终端。这一转变的核心支撑&#xff0c;来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒&#xff08;T-Box&#xff09;方案&#xff1a;NXP S32K146 与…...

Java数值运算常见陷阱与规避方法

整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...