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<T extends Authentication> implements AuthenticationUserDetailsService<T> {
// 结构器注入
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;}
}
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<String, ?> convertUserAuthentication(Authentication authentication) {Map<String, Object> 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<String, ?> 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<String, ?> 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<TokenEnhancer> 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