Spring|Spring Security Oauth2协议扩展授权模式之通过自定义字段刷新access_token
1、Spring Security如何优雅的增加OAuth2协议授权模式?
当前教程是关于如何通过自定义字段刷新access_token的,要扩展授权模式请参考这位博主的教程:https://www.cnblogs.com/zlt20...
2、为什么要自定义刷新access_token的关键字段
Oauth2协议默认的刷新access_token流程就是仅通过唯一username进行刷新access_token的。当我们模仿password授权模式扩展出比如手机号/邮箱+密码登录的授权模式且前端不使用username字段或username字段允许重复时,此时我们就不能通过username字段进行刷新access_token,需要在access_token中携带一个唯一的字段比如userId或mobile提供给授权服务器刷新token使用。因为刷新token与生成token的流程仅有小部分不同。
关于Spring Security Oauth2 认证(获取token/刷新token)流程(password模式)分析,请移步:https://blog.csdn.net/bluuuse...
3、实现过程
3.1 整个拷贝UserDetailsByNameServiceWrapper这个类的内容做如下扩展,主要修改loadUserDetails()方法。
package com.nowenti.auth.security.service;
import cn.hutool.core.convert.Convert;
import com.nowenti.auth.security.exception.UserIdNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
/**
* @Description: 自定义扩展的授权模式专用ServiceWrapper
* 通过用户id加载用户details -> 用于通过user_id刷新token
* 自定义当前ServiceWrapper的目的是让刷新token时可以选择通过用户名或用户id去加载UserDetails
* @version 1.0
* @author owen
* @email 975706304@qq.com
* @date 2021/8/16 23:04
*/
public class UserDetailsByNameOrIdServiceWrapper implements AuthenticationUserDetailsService {// 构造器注入
private ICustomUserDetailsService userDetailsService;
public UserDetailsByNameOrIdServiceWrapper() {
}public UserDetailsByNameOrIdServiceWrapper(ICustomUserDetailsService userDetailsService) {
Assert.notNull(userDetailsService, "userDetailsService cannot be null.");
this.userDetailsService = userDetailsService;
}/**
* 加载用户详情对象
* authentication.getName()的值有两种情况,需要手动区分
*1、user_name
*2、user_id
*3、任意自定义字段
* @param authentication
* @return
* @throws UserIdNotFoundException
*/
@Override
public UserDetails loadUserDetails(T authentication) {
// 从PreAuthenticatedAuthenticationToken获取Principle
// Principle是 -> UsernamePasswordAuthenticationToken对象
// UsernamePasswordAuthenticationToken对象的Principle就是nameOrId
String usernameOrUserId = authentication.getName();
try {
// 能正确转换成Long型用户id -> 会员用户
Long userId = Convert.toLong(usernameOrUserId);
return this.userDetailsService.loadMemberUserById(userId);
} catch (Exception e) {
// 转换异常,usernameOrUserId为用户名 -> 系统用户
return this.userDetailsService.loadUserByUsername(usernameOrUserId);
}}public void setUserDetailsService(ICustomUserDetailsService aUserDetailsService) {
this.userDetailsService = aUserDetailsService;
}
}
【Spring|Spring Security Oauth2协议扩展授权模式之通过自定义字段刷新access_token】3.2 整个拷贝DefaultUserAuthenticationConverter这个类的内容做如下扩展,主要修改extractAuthentication()方法
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package com.nowenti.auth.security.converter;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import com.nowenti.auth.security.service.ICustomUserDetailsService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;
/**
* @Description: 自定义的用户认证转换器
* 自定义当前转换器的目的是为了让自定义的授权模式支持通过user_id刷新token
* @version 1.0
* @author owen
* @email 975706304@qq.com
* @date 2021/8/19 10:06
*/
public class CustomUserAuthenticationConverter implements UserAuthenticationConverter {
private Collection extends GrantedAuthority> defaultAuthorities;
private ICustomUserDetailsService userDetailsService;
public CustomUserAuthenticationConverter() {
}/**
* 构造器注入userDetailsService
* @param userDetailsService
*/
public void setUserDetailsService(ICustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}public void setDefaultAuthorities(String[] defaultAuthorities) {
this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities));
}/**
* 将UsernamePasswordAuthenticationToken对象转换成普通map
* 该map的内容将被添加进access_token和refresh_token
* 该方法后于extractAuthentication()执行
* 该方法登录和刷新token都会执行
* @param authentication
* @return
*/
@Override
public Map convertUserAuthentication(Authentication authentication) {
Map response = new LinkedHashMap<>();
response.put("user_name", authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}return response;
}/**
* 将token map提取成UsernamePasswordAuthenticationToken对象
* 该方法先于convertUserAuthentication()执行
* 该方法仅刷新token执行
* @param map
* @return
*/
@Override
public Authentication extractAuthentication(Map map) {
// 先判断token map中是否包含user_name字段,包含表示是系统用户刷新token
// 由于两种用户token中都有user_id字段,所以先判断用户名
if (map.get("user_name") != null) {
Object principal = map.get("user_name");
Collection extends GrantedAuthority> authorities = this.getAuthorities(map);
if (this.userDetailsService != null) {
UserDetails user = this.userDetailsService.loadUserByUsername((String) map.get("user_name"));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
} else if (map.get("user_id") != null) {
// token map中包含不包含user_name字段,表示是会员用户刷新token
Object principal = map.get("user_id");
Collection extends GrantedAuthority> authorities = this.getAuthorities(map);
if (this.userDetailsService != null) {
UserDetails user = this.userDetailsService.loadMemberUserById((Long) map.get("user_id"));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
} else {
return null;
}
}private Collection extends GrantedAuthority> getAuthorities(Map map) {
if (!map.containsKey("authorities")) {
return this.defaultAuthorities;
} else {
Object authorities = map.get("authorities");
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
} else if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection) authorities));
} else {
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
}
}
4、授权服务器AuthorizationServerConfig配置 4.1 注入自定义的DefaultTokenServices实现类bean
/**
*注入自定义的DefaultTokenServices实现类对象
* @param endpoints
* @return
*/
@Bean
public DefaultTokenServices customTokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// access_token有效期:2个小时 -> 60*60*2
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 2);
// refresh_token有效期:12个小时 -> 60*60*12
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 12);
// 设置自定义的UserDetailsByNameOrIdServiceWrapper
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameOrIdServiceWrapper<>(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
}
return tokenServices;
}
4.2 注入自定义的CustomUserAuthenticationConverter
/**
* 注入自定义的CustomUserAuthenticationConverter
* @return
*/
@Bean
public DefaultAccessTokenConverter defaultAccessTokenConverter() {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
return defaultAccessTokenConverter;
}
4.3 将所有修改配置进configure(AuthorizationServerEndpointsConfigurer endpoints)
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(userDetailsService)
// refresh token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
//1、重复使用:access token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
//2、非重复使用:access token过期刷新时, refresh token过期时间延续,在refresh token有效期内刷新便永不失效达到无需再次登录的目的
.reuseRefreshTokens(true)
// 将所有授权模式添加到配置中
.tokenGranter(createTokenGranter(endpoints))
// 配置自定义的CustomTokenServices实现类
.tokenServices(customTokenServices(endpoints))
// 配置自定义的用户认证转换器
.accessTokenConverter(defaultAccessTokenConverter());
}
5、刷新token的相关方法调用链 5.1 loadUserDetails()调用链
文章图片
5.1 extractAuthentication()调用链
文章图片
6、打完收工 6.1 @author
wx : owen2505
email : 975706304@qq.com
推荐阅读
- Activiti(一)SpringBoot2集成Activiti6
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- 2018-07-09|2018-07-09 Spring 的DBCP,c3p0
- spring|spring boot项目启动websocket
- Spring|Spring Boot 整合 Activiti6.0.0
- Spring集成|Spring集成 Mina
- springboot使用redis缓存
- Spring|Spring 框架之 AOP 原理剖析已经出炉!!!预定的童鞋可以识别下发二维码去看了
- Spring|Spring Boot之ImportSelector