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 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 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 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 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()调用链
Spring|Spring Security Oauth2协议扩展授权模式之通过自定义字段刷新access_token
文章图片

5.1 extractAuthentication()调用链
Spring|Spring Security Oauth2协议扩展授权模式之通过自定义字段刷新access_token
文章图片

6、打完收工 6.1 @author
wx : owen2505
email : 975706304@qq.com

    推荐阅读