版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
前言
之前,已经解决了自定义的token获取,包含重写了通过password和手机号获取token的方式和代码。一切运行良好。由于使用了jwt,所以自定义了DefaultUserAuthenticationConverter(token转化器和增强器)为自己的token增加一些字段,如:uid和其它的字段。
token 扩展 CustomUserAuthenticationConverter
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
/**
* 拓展jwt
*
* @param authentication
* @return
*/
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
String username = "user_name";
String appId = "appId";
String phone = "phone";
String uid = "uid";
String appUserId = "appUserId";
String countryCode = "countryCode";
LinkedHashMap map = new LinkedHashMap();
Object principal = authentication.getPrincipal();
if (principal instanceof OAuthUserDetail) {
OAuthUserDetail userJwt = (OAuthUserDetail) principal;
map.put(username, userJwt.getUsername());
map.put(appId, userJwt.getAppId());
map.put(phone, userJwt.getPhone());
map.put(uid, userJwt.getId());
map.put(appUserId, userJwt.getAppUserId());
map.put(countryCode, userJwt.getCountryCode());
}
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
map.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
ServletRequestAttributes attr =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();
map.put("clientIP", ServletUtil.getClientIP(request));
return map;
}
}
我们通过正常的方式获取token;
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOm51bGwsImF1ZCI6WyJwcm9kdWN0X2FwaSJdLCJwaG9uZSI6IjEwMDg2MTExIiwidXNlcl9uYW1lIjoidGVzdCIsImNvdW50cnlDb2RlIjoiODYiLCJhcHBJZCI6ImFwcElkIiwiY2xpZW50SVAiOiIxMjcuMC4wLjEiLCJzY29wZSI6WyJyZWFkIiwiIHdyaXRlIl0sImFwcFVzZXJJZCI6IjEwMDg2MTExIiwiZXhwIjoxNjQ2NjAyNDY3LCJqdGkiOiJiYjkyYWJiOS04NThmLTQ1YzMtYjEwNy04ZDY0ZTIwNjlhMTciLCJjbGllbnRfaWQiOiJib2JvX29uZSJ9.EJoZ4P2fwLE95G67y7OmBupzaaxGD4iKAOyLKxpJudjfTfWlclY9bzaAI1J9tBTybK2nG9Nc6sVMZlPEw4PoIOY_gJ9-wn0Gxe7BzvpL6cfNW2wSKIl37ZRv0-kr5zLEhPHeCtc8wqmbtTPWAYecPAShEL5ItM_q5aAm3lXa7LD246vNjTUecv7JR8vgLUcyjHpgeG0xW9xAH7Dp6Wh4Ml7k4sRz6UW397P5FANUtvIxV5Db-8bsnBdQeBwdsPRZXdvWfGypRPUf9LShFK-bl7g8UUsqXkSZoikFreb7NiCbzGiWu7qMz_GTk2zv1SsfqIlorZr5jdh2b3b1R9kdvA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ0ZXN0IiwiY2xpZW50X2lkIjoiYm9ib19vbmUiLCJ1aWQiOm51bGwsImF1ZCI6WyJwcm9kdWN0X2FwaSJdLCJwaG9uZSI6IjEwMDg2MTExIiwiY291bnRyeUNvZGUiOiI4NiIsImFwcElkIjoiYXBwSWQiLCJjbGllbnRJUCI6IjEyNy4wLjAuMSIsInNjb3BlIjpbInJlYWQiLCIgd3JpdGUiXSwiYXRpIjoiYmI5MmFiYjktODU4Zi00NWMzLWIxMDctOGQ2NGUyMDY5YTE3IiwiYXBwVXNlcklkIjoiMTAwODYxMTEiLCJleHAiOjE2NzkwMDI0NjcsImp0aSI6Ijk4NGNjMmEzLWNmNjYtNDZkOC04OGZkLTdhMWI5YmFlNTlhOSJ9.mGpVJTvob9RRpccoEvY-6c4gWJuCQg04nr0GJxiupz8z-bj56EFyAScZoCI8uGPCzdEHym7Ey4F3_CnCdLAbI41rTYq51ea-izCkUBigtnm0Y02Uc1A56GGPuOJ5Wz6pIC7sRaIhWM8HltNqjCuu7TenMRyQ8dnnEE9-aUKlAWMT5o7RHpJX42R43kaD8XJVhxCRq-AfQAUOTG4N1SxAVKmM0YY_m5vIvy7aPKsmf8kz-UYPswA6klEbRWw9ELlpNUPnX4APQHqej00d6C6KvWBB0B5bOoDccC6e2anB1VpIXpj1_QXW2kc8WVSoIyhFVcsjEjXcglBU1EcFdGGkcg",
"expires_in": 3599999,
"scope": "read write",
"jti": "bb92abb9-858f-45c3-b107-8d64e2069a17"
解析token
可以看到,自定义参数正常。是可以解析出来的。
刷新token获取新的token。问题
根据刷新token获取新的token,但是新的token解析出来,会发现缺少参数。
原因:
刷新token的逻辑,在笔者最开始的看法。就是解析一个token,修改一下它的过期时间并生成一个新的token返还回去就行了。
其实,在SpringSecurityOauth的之前版本(具体哪个版本笔者也没有过多的纠结),就是采取这种简单的方式,直接读取原token的数据,修改过期时间然后返回。
代码:DefaultTokenServices 这个类
老版本的处理方式:
@Override
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
throws AuthenticationException {
//验证是否是刷新token
if (!supportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
// 读取token
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
if (refreshToken == null) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
OAuth2Authentication authentication =
tokenStore.readAuthenticationForRefreshToken(refreshToken);
String clientId = authentication.getOAuth2Request().getClientId();
if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
}
// clear out any access tokens already associated with the refresh
// token.
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
if (isExpired(refreshToken)) {
tokenStore.removeRefreshToken(refreshToken);
throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
}
// 根据读取到的数据生成新的authentication
authentication = createRefreshedAuthentication(authentication, tokenRequest);
if (!reuseRefreshToken) {
tokenStore.removeRefreshToken(refreshToken);
refreshToken = createRefreshToken(authentication);
}
// 创建新的token
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
if (!reuseRefreshToken) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
可以看到,就是一个简单的读取,重置。
但是在新的版本中,这个逻辑进行了修改,只读取原token里面的username,然后再根据loadByUsername这个通用的方法进行数据查询,最后再返回数据。
新版本:
增加了:AuthenticationManager
@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
throws AuthenticationException {
if (!supportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
// 这一步还是读取token里面的信息
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
if (refreshToken == null) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) {
// The client has already been authenticated, but the user authentication might be old now, so give it a
// chance to re-authenticate.
Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
// 但是在这里发生了变化,需要通过authenticationManager里面的authenticate
user = authenticationManager.authenticate(user);
Object details = authentication.getDetails();
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
authentication.setDetails(details);
}
String clientId = authentication.getOAuth2Request().getClientId();
if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
}
// clear out any access tokens already associated with the refresh
// token.
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
if (isExpired(refreshToken)) {
tokenStore.removeRefreshToken(refreshToken);
throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
}
authentication = createRefreshedAuthentication(authentication, tokenRequest);
if (!reuseRefreshToken) {
tokenStore.removeRefreshToken(refreshToken);
refreshToken = createRefreshToken(authentication);
}
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
if (!reuseRefreshToken) {
tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
}
return accessToken;
}
新的版本需要通过传入的AuthenticationManager 里面的 authenticate 方法去重新生成一个Authentication对象。在根据这个对象去生成新的token。
java.lang.IllegalStateException: UserDetailsService is required.
由于我们并没有去重写AuthenticationManager,所以会调用系统自己写的ProviderManager
最终会调到 UserDetailsService 里面的loadUserByUsername方法。
好家伙,又回到了最初的起点。
于是我们需要重写一个AuthenticationManager,自己生成一个Authentication给他。以及它的AuthenticationProvider。。。。
那有没有简单一点的实现呢???
其实是有的,之前说了,在OAuth2的之前版本是不需要验证这个的,那么我们直接自定义一个DefaultTokenServices,把代码copy过来,再把刷新token的部分用老版本代码的逻辑不久可以了吗?
它实际上就是返回一个Authentication,我们不查库,通过读取刷新token生成新token也是一样的。
于是自定义:CustomDefaultTokenServices
将refreshAccessToken 里面的代码逻辑替换。
最后在配置里面指定:
@Bean
public CustomDefaultTokenServices customTokenServices() {
CustomDefaultTokenServices tokenServices = new CustomDefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setAuthenticationManager(authenticationManager);
tokenServices.setTokenEnhancer(accessTokenConverter());
// 设置刷新token的数据源来源jdbc
tokenServices.setClientDetailsService(jdbcClientDetailsService());
return tokenServices;
}
private List<TokenGranter> getDefaultTokenGranters(
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetails,
AuthorizationCodeServices codeServices,
OAuth2RequestFactory requestFactory) {
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(
new AuthorizationCodeTokenGranter(
tokenServices, codeServices, clientDetails, requestFactory));
// 自定义的refreshTokenGranter 使用自定义的tokenService
tokenGranters.add(
new RefreshTokenGranter(customTokenServices(), clientDetails, requestFactory));
ImplicitTokenGranter implicit =
new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(
new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
// 自定义用户密码登录管理器
CustomAccountPasswordTokenGranter usernampasswordTokenGranter =
new CustomAccountPasswordTokenGranter(
tokenServices, clientDetails, requestFactory, service);
tokenGranters.add(usernampasswordTokenGranter);
// 自定义验证码登录管理器
CustomPhoneSmsTokenGranter phoneSmsTokenGranter =
new CustomPhoneSmsTokenGranter(tokenServices, clientDetails, requestFactory, service);
tokenGranters.add(phoneSmsTokenGranter);
return tokenGranters;
}
刷新问题
后来仔细想了想,OAuth新版本刷新token获取新token,新增了一步查询是很有必要的。
如果用户更新了自己的数据,这个数据我们又封装在了jwt里面,例如手机号:如果没有重写登录的话。那么它通过刷新token获取的手机号不会更新。
OAuth也是经过了这番考虑,所以新增了一个查询的过程。
解决
从转化入手,转化的时候查询就可以了。
@Component
@Slf4j
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
private Collection<? extends GrantedAuthority> defaultAuthorities;
@Resource UserService UserService;
/**
* 拓展jwt
*
* @param authentication
* @return
*/
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
String username = "user_name";
String appId = "appId";
String phone = "phone";
String uid = "uid";
String appUserId = "appUserId";
String countryCode = "countryCode";
LinkedHashMap map = new LinkedHashMap();
Object principal = authentication.getPrincipal();
if (principal instanceof OAuthUserDetail) {
OAuthUserDetail userJwt = (OAuthUserDetail) principal;
map.put(username, userJwt.getUsername());
map.put(appId, userJwt.getAppId());
map.put(phone, userJwt.getPhone());
map.put(uid, userJwt.getId());
map.put(appUserId, userJwt.getAppUserId());
map.put(countryCode, userJwt.getCountryCode());
}
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
map.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
ServletRequestAttributes attr =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();
map.put("clientIP", ServletUtil.getClientIP(request));
return map;
}
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
log.info("extractAuthentication-->map,{}", map);
String uid = MapUtil.getStr(map, "uid");
if (StrUtil.isNotBlank(uid)) {
// 通过uid 和 appId 重新查询数据库获取用户信息,避免刷新token获取过程中用户更新信息
String appId = MapUtil.getStr(map, "appId");
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
OAuthUserDetail userOAuthByUid = UserService.getUserOAuthByUid(uid, appId);
return new UsernamePasswordAuthenticationToken(userOAuthByUid, "N/A", authorities);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
if (!map.containsKey(AUTHORITIES)) {
return defaultAuthorities;
}
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(
StringUtils.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
在刷新token里面:提取身份验证的时候会读取到这个方法:extractAuthentication
我们重写这个方法进行查库,问题便解决了。
结尾
至此,OAuth2的自定义环节就结束了。代码成功运行。对于Oauth2,也清晰了很多,但是总感觉这总写法有些许取巧,没有完全按照它的规则来。不知道有没有其它更好的办法,也欢迎交流。