自定义的SpringSecurityOAuth(刷新token版本迭代问题)

1,506 阅读5分钟

版本

        <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,也清晰了很多,但是总感觉这总写法有些许取巧,没有完全按照它的规则来。不知道有没有其它更好的办法,也欢迎交流。