文章目录

  • AuthorizationServer
    • 引言
      • AuthorizationServerTokenServices
      • ResourceServerTokenServices
      • TokenStore
      • TokenEnhancer
    • 自定义 TokenGranter
      • 代码实现 - CustomTokenGranter
    • 自定义 AuthorizationServerTokenServices
      • 代码实现 - CustomAuthorizationServerTokenServices
    • 自定义 TokenStore
      • JwtAccessTokenConverter (TokenEnhancer & AccessTokenConverter)
        • 签名与校验
      • 代码实现 - JwtTokenStore
  • ResourceServer
    • 引言
      • ResourceServerTokenServices
    • 调整 AuthorizationServer 的 CustomAuthorizationServerTokenServices
    • 自定义 ResourceServer 的响应格式 - ResourceServerConfiguration
  • 后记

AuthorizationServer

引言

本文在 前一篇 基础上构建.

  • 全面的令牌自定义, 包含:

    • AuthorizationServerTokenSerivces 的自定义;
    • TokenStore 的自定义, TokenEnhancer自定义;
    • OAuth2AccessToken 的自定义;
  • 启用 JWT (Json Web Token);

在前面的 DEMO 中, 我们已经自定义了 TokenGranter:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {// .../*** Description: 配置 {@link AuthorizationServerEndpointsConfigurer}<br>* Details: 配置授权服务器端点的非安全性特性, 例如 令牌存储, 自定义. 如果是密码授权, 需要在这里提供一个 {@link AuthenticationManager}** @see AuthorizationServerConfigurerAdapter#configure(AuthorizationServerEndpointsConfigurer)*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {// @formatter:off// 对于密码授权模式, 需要提供 AuthenticationManager 用于用户信息的认证endpoints.authenticationManager(authenticationManager)// ~ 自定义的 WebResponseExceptionTranslator, 默认使用 DefaultWebResponseExceptionTranslator, 在 /oauth/token 端点//   ref: TokenEndpoint.exceptionTranslator(webResponseExceptionTranslator)// ~ 自定义的 TokenGranter.tokenGranter(new CustomTokenGranter(endpoints, authenticationManager))// .tokenServices(AuthorizationServerTokenServices)// .tokenStore(TokenStore)// .tokenEnhancer(TokenEnhancer)// ~ refresh_token required.userDetailsService(userDetailsService);// @formatter:on}// ...}

CustomTokenGranter 自身委托 CompositeTokenGranter 来颁发令牌.

AuthorizationServerTokenServices

AuthorizationServerTokenServices 为授权服务器提供 “创建”, “更新”, “获取” OAuth2AccessToken 的方法接口. 主要职责上是把认证信息 (Authentication) "塞"到 AccessToken 中.

默认实现: DefaultTokenServices (org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultAuthorizationServerTokenServices). 支持AuthorizationServerEndpointsConfigurer#tokenServices(AuthorizationServerTokenServices) 自定义配置.

来看看 AuthorizationServerTokenServices 接口的方法签名:

// 根据指定的凭证信息, 创建 AccessToken
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;// 刷新 AccessToken.
// 第二个参数: 认证请求 (TokenRequest) 被用来验证原来 AccessToken 中的客户端ID是否与刷新请求中的一致, 和用于缩小 Scope
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException;// 从 OAuth2Authentication 中获取 OAuth2AccessToken
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

对于每一种 TokenGranter 的实现 (AuthorizationCodeTokenGranter, RefreshTokenGranter, ImplicitTokenGranter, ClientCredentialsTokenGranter, ResourceOwnerPasswordTokenGranter), 在 OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) 方法最后, 会调用 TokenServicesOAuth2AccessToken createAccessToken(OAuth2Authentication authentication) 构建 OAuth2AccessToken 对象:

public abstract class AbstractTokenGranter implements TokenGranter {//...public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {if (!this.grantType.equals(grantType)) {return null;}String clientId = tokenRequest.getClientId();ClientDetails client = clientDetailsService.loadClientByClientId(clientId);validateGrantType(grantType, client);return getAccessToken(client, tokenRequest);}protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));}//...}

ResourceServerTokenServices

ResourceServerTokenServices 默认有两个实现, 一个是 RemoteTokenServices, 另外一个是 DefaultTokenServices.

  • 在授权服务器, 我们自定义的 CustomAuthorizationServerTokenServices 继承的 DefaultTokenServices 也实现了 AuthorizationServerTokenServicesResourceServerTokenServices 两个接口, 前者用于接收朝向授权服务器的令牌申请, 刷新请求; 而 ResourceServerTokenServices 提供的接口方法则是用于处理远端资源服务器的解析令牌的请求 (ref: CheckTokenEndpoint).

TokenStore

针对 OAuth2 令牌的持久化的接口. Spring Security OAuth 2.0 提供了好几个开箱即用的实现.

默认 DefaultTokenServices 通过 TokenStore 来执行令牌的持久化操作.

TokenEnhancer

TokenEnhancer 提供了一个在 OAuth2AccessToken 构建之前, 自定义它的机制. 翻阅 DefaultTokenServices 我们可以看到 TokenEnhancer 的使用时机:

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,ConsumerTokenServices, InitializingBean {//...private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());if (validitySeconds > 0) {token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));}token.setRefreshToken(refreshToken);token.setScope(authentication.getOAuth2Request().getScope());return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;}//...
}

۞ 现在让我们来梳理一下它们之间的关系:

  • Spring Security OAuth 2.0 的授权服务器通过 TokenGranter (根据授权类型 Grant Type) 调用匹配的 Granter 的 grant 方法构建 OAuth2AccessToken;

  • grant 方法构建令牌对象的逻辑是通过 AuthorizationServerTokenServices 的 createAccessToken 实现的;

  • 在默认的实现类 DefaultTokenServices 中, 需要调用 TokenStore 提供的各个用于持久化方法接口来操作令牌;

  • DefaultTokenServices 的 createAccessToken 最后, 还调用了 TokenEnhancer 来 “增强” 令牌 (一般是塞入一些额外的信息);

本文主要着力于自定义以上这一套流程, 完全接管令牌的生命周期, 并使用 Json Web Token 作为令牌载体.

自定义 TokenGranter

TokenGranter 入手, 在 上一篇 的 DEMO 中是已经使用到了自定义的 TokenGranter. 本章我们稍作介绍.

TokenGranter 是一个定义令牌颁发实现类的标准接口, 它有众多的实现类. 先来一瞥:

继承超类 AbstractTokenGranter 的 5 个实现类分别对应 4 种授权类型的实现和刷新令牌的生成器 (RefreshTokenGranter); CompositeTokenGranter 是一个组合类, 根据授权类型的不同, 调用不同的实现类. 也是 AuthorizationServerEndpointsConfigurer 的默认 Granter.

而我们自己实现的 Granter, 借鉴了 CompositeTokenGranter 的机制, 委托它来构建令牌对象, 并在这个基础上, 采用了自定义的 OAuth2AccessToken, 并重写序列化方法以与我们自定义的统一响应结构吻合.

代码实现 - CustomTokenGranter

/*** 自定义的 {@link TokenGranter}<br>* 为了自定义令牌的返回结构 (把令牌信息包装到通用结构的 data 属性内).** <pre>* {*     "status": 200,*     "timestamp": "2020-06-23 17:42:12",*     "message": "OK",*     "data": "{\"additionalInformation\":{},\"expiration\":1592905452867,\"expired\":false,\"expiresIn\":119,\"scope\":[\"ACCESS_RESOURCE\"],\"tokenType\":\"bearer\",\"value\":\"81b0d28f-f517-4521-b549-20a10aab0392\"}"* }* </pre>** @author LiKe* @version 1.0.0* @date 2020-06-23 14:52* @see org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken(Principal, Map)* @see org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#getAccessToken(Principal, Map)* @see CompositeTokenGranter*/
@Slf4j
public class CustomTokenGranter implements TokenGranter {/*** 委托 {@link CompositeTokenGranter}*/private final CompositeTokenGranter delegate;/*** Description: 构建委托对象 {@link CompositeTokenGranter}** @param configurer            {@link AuthorizationServerEndpointsConfigurer}* @param authenticationManager {@link AuthenticationManager}, grantType 为 password 时需要* @author LiKe* @date 2020-06-23 15:28:24*/public CustomTokenGranter(AuthorizationServerEndpointsConfigurer configurer, AuthenticationManager authenticationManager) {final ClientDetailsService clientDetailsService = configurer.getClientDetailsService();final AuthorizationServerTokenServices tokenServices = configurer.getTokenServices();final AuthorizationCodeServices authorizationCodeServices = configurer.getAuthorizationCodeServices();final OAuth2RequestFactory requestFactory = configurer.getOAuth2RequestFactory();this.delegate = new CompositeTokenGranter(Arrays.asList(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory),new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory),new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory),new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory),new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory)));}@Overridepublic OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {log.debug("Custom TokenGranter :: grant token with type {}", grantType);// 如果发生异常, 会触发 WebResponseExceptionTranslatorfinal OAuth2AccessToken oAuth2AccessToken =Optional.ofNullable(delegate.grant(grantType, tokenRequest)).orElseThrow(() -> new UnsupportedGrantTypeException("不支持的授权类型!"));return new CustomOAuth2AccessToken(oAuth2AccessToken);}/*** 自定义 {@link CustomOAuth2AccessToken}*/@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = CustomOAuth2AccessTokenJackson2Serializer.class)public static final class CustomOAuth2AccessToken extends DefaultOAuth2AccessToken {private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();public CustomOAuth2AccessToken(OAuth2AccessToken accessToken) {super(accessToken);}/*** Description: 序列化 {@link OAuth2AccessToken}** @return 形如 { "access_token": "aa5a459e-4da6-41a6-bf67-6b8e50c7663b", "token_type": "bearer", "expires_in": 119, "scope": "read_scope" } 的字符串* @see OAuth2AccessTokenJackson1Serializer*/@SneakyThrowspublic String tokenSerialize() {final LinkedHashMap<Object, Object> map = new LinkedHashMap<>(5);map.put(OAuth2AccessToken.ACCESS_TOKEN, this.getValue());map.put(OAuth2AccessToken.TOKEN_TYPE, this.getTokenType());final OAuth2RefreshToken refreshToken = this.getRefreshToken();if (Objects.nonNull(refreshToken)) {map.put(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());}final Date expiration = this.getExpiration();if (Objects.nonNull(expiration)) {map.put(OAuth2AccessToken.EXPIRES_IN, (expiration.getTime() - System.currentTimeMillis()) / 1000);}final Set<String> scopes = this.getScope();if (!CollectionUtils.isEmpty(scopes)) {final StringBuffer buffer = new StringBuffer();scopes.stream().filter(StringUtils::isNotBlank).forEach(scope -> buffer.append(scope).append(" "));map.put(OAuth2AccessToken.SCOPE, buffer.substring(0, buffer.length() - 1));}final Map<String, Object> additionalInformation = this.getAdditionalInformation();if (!CollectionUtils.isEmpty(additionalInformation)) {additionalInformation.forEach((key, value) -> map.put(key, additionalInformation.get(key)));}return OBJECT_MAPPER.writeValueAsString(map);}}/*** 自定义 {@link CustomOAuth2AccessToken} 的序列化器*/private static final class CustomOAuth2AccessTokenJackson2Serializer extends StdSerializer<CustomOAuth2AccessToken> {protected CustomOAuth2AccessTokenJackson2Serializer() {super(CustomOAuth2AccessToken.class);}@Overridepublic void serialize(CustomOAuth2AccessToken oAuth2AccessToken, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {jsonGenerator.writeStartObject();jsonGenerator.writeObjectField(SecurityResponse.FIELD_HTTP_STATUS, HttpStatus.OK.value());jsonGenerator.writeObjectField(SecurityResponse.FIELD_TIMESTAMP, LocalDateTime.now().format(DateTimeFormatter.ofPattern(SecurityResponse.TIME_PATTERN, Locale.CHINA)));jsonGenerator.writeObjectField(SecurityResponse.FIELD_MESSAGE, HttpStatus.OK.getReasonPhrase());jsonGenerator.writeObjectField(SecurityResponse.FIELD_DATA, oAuth2AccessToken.tokenSerialize());jsonGenerator.writeEndObject();}}
}

自定义 AuthorizationServerTokenServices

从上一章可以看到, 对于每个授权类型对应的具体的 Granter, 都构造依赖 tokenServices. 所有 “具体的” Granter 都继承于 AbstractTokenGranter, 后者在构建 OAuth2AccessToken 之前会调用 tokenServices 的 createAccessToken. 本身, 对于令牌的 “业务性” 操作都是委托 tokenServices 来进行的. 而 “持久化” 操作, 则是委托 TokenStore 来完成.

代码实现 - CustomAuthorizationServerTokenServices

这是我们自定的 AuthorizationServerTokenServices 完整代码:

/*** 自定义的 {@link AuthorizationServerTokenServices}** @author LiKe* @version 1.0.0* @date 2020-07-08 14:59* @see org.springframework.security.oauth2.provider.token.DefaultTokenServices* @see AuthorizationServerTokenServices*/
public class CustomAuthorizationServerTokenServices extends DefaultTokenServices {// ~ Necessary// -----------------------------------------------------------------------------------------------------------------/*** 自定义的持久化令牌的接口 {@link TokenStore} 引用*/private final TokenStore tokenStore;/*** 自定义的 {@link ClientDetailsService} 的引用*/private final ClientDetailsService clientDetailsService;// ~ Optional// -----------------------------------------------------------------------------------------------------------------/*** {@link AuthenticationManager}*/private AuthenticationManager authenticationManager;// =================================================================================================================/*** {@link TokenEnhancer}*/private final TokenEnhancer accessTokenEnhancer;private final TokenGenerator tokenGenerator = new TokenGenerator();/*** Description: 构建 {@link AuthorizationServerTokenServices}<br>* Details: 依赖 {@link TokenStore}, {@link org.springframework.security.oauth2.provider.ClientDetailsService}\** @param endpoints           {@link AuthorizationServerEndpointsConfigurer}* @author LiKe* @date 2020-07-08 15:24:18*/public CustomAuthorizationServerTokenServices(AuthorizationServerEndpointsConfigurer endpoints) {this.tokenStore = Objects.requireNonNull(endpoints.getTokenStore(), "tokenStore 不能为空!");this.clientDetailsService = Objects.requireNonNull(endpoints.getClientDetailsService(), "clientDetailsService 不能为空!");final TokenEnhancer tokenEnhancer = Objects.requireNonNull(endpoints.getTokenEnhancer(), "tokenEnhancer 不能为空!");Assert.assignable(JwtAccessTokenConverter.class, tokenEnhancer.getClass(), () -> new RuntimeException("tokenEnhancer 必须是 JwtAccessTokenConverter 的实例!"));this.accessTokenEnhancer = tokenEnhancer;}/*** 创建 access-token** @see org.springframework.security.oauth2.provider.token.DefaultTokenServices#createAccessToken(OAuth2Authentication)*/@Overridepublic OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {// 当前客户端是否支持 refresh_tokenfinal boolean supportRefreshToken = isSupportRefreshToken(authentication);OAuth2RefreshToken existingRefreshToken = null;// 如果已经存在令牌final OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);if (Objects.nonNull(existingAccessToken)) {if (existingAccessToken.isExpired()) {// 如果已过期, 则删除 AccessToken 和 RefreshTokenif (supportRefreshToken) {existingRefreshToken = existingAccessToken.getRefreshToken();tokenStore.removeRefreshToken(existingRefreshToken);}tokenStore.removeAccessToken(existingAccessToken);} else {// 否则重新保存令牌 (以防 authentication 已经改变)tokenStore.storeAccessToken(existingAccessToken, authentication);return existingAccessToken;}}// 生成新的 refresh_tokenOAuth2RefreshToken newRefreshToken = null;if (supportRefreshToken) {if (Objects.isNull(existingRefreshToken)) {// 如果没有 RefreshToken, 生成一个newRefreshToken = tokenGenerator.createRefreshToken(authentication);} else if (existingRefreshToken instanceof ExpiringOAuth2RefreshToken) {// 如果有 RefreshToken 但是已经过期, 重新颁发if (System.currentTimeMillis() > ((ExpiringOAuth2RefreshToken) existingRefreshToken).getExpiration().getTime()) {newRefreshToken = tokenGenerator.createRefreshToken(authentication);}}}// 生成新的 access_tokenfinal OAuth2AccessToken newAccessToken = tokenGenerator.createAccessToken(authentication, newRefreshToken);if (supportRefreshToken) {tokenStore.storeRefreshToken(newRefreshToken, authentication);}tokenStore.storeAccessToken(newAccessToken, authentication);return newAccessToken;}/*** 刷新 access-token** @see org.springframework.security.oauth2.provider.token.DefaultTokenServices#refreshAccessToken(String, TokenRequest)*/@Overridepublic OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {final String clientId = tokenRequest.getClientId();if (Objects.isNull(clientId) || !StringUtils.equals(clientId, tokenRequest.getClientId())) {throw new InvalidGrantException(String.format("错误的客户端: %s, refresh token: %s", clientId, refreshTokenValue));}if (!isSupportRefreshToken(clientId)) {throw new InvalidGrantException(String.format("客户端 (%s) 不支持 refresh_token!", clientId));}final OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);if (Objects.isNull(refreshToken)) {throw new InvalidTokenException(String.format("无效的 refresh_token: %s!", refreshTokenValue));}// ~ 用 refresh_token 获取 OAuth2 认证信息OAuth2Authentication oAuth2Authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);if (Objects.nonNull(this.authenticationManager) && !oAuth2Authentication.isClientOnly()) {oAuth2Authentication = new OAuth2Authentication(oAuth2Authentication.getOAuth2Request(),authenticationManager.authenticate(new PreAuthenticatedAuthenticationToken(oAuth2Authentication.getUserAuthentication(), StringUtils.EMPTY, oAuth2Authentication.getAuthorities())));oAuth2Authentication.setDetails(oAuth2Authentication.getDetails());}tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);if (isExpired(refreshToken)) {tokenStore.removeRefreshToken(refreshToken);throw new InvalidTokenException("无效的 refresh_token (已过期)!");}// ~ 刷新 OAuth2 认证信息, 并基于此构建新的 OAuth2AccessTokenoAuth2Authentication = createRefreshedAuthentication(oAuth2Authentication, tokenRequest);// 获取新的 refresh_tokenfinal OAuth2AccessToken refreshedAccessToken = tokenGenerator.createAccessToken(oAuth2Authentication, refreshToken);tokenStore.storeAccessToken(refreshedAccessToken, oAuth2Authentication);return refreshedAccessToken;}@Overridepublic OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {return tokenStore.getAccessToken(authentication);}// -----------------------------------------------------------------------------------------------------------------/*** Description: 判断当前客户端是否支持 refreshToken** @param authentication {@link OAuth2Authentication}* @return boolean* @author LiKe* @date 2020-07-08 18:16:09*/private boolean isSupportRefreshToken(OAuth2Authentication authentication) {return isSupportRefreshToken(authentication.getOAuth2Request().getClientId());}/*** Description: 判断当前客户端是否支持 refreshToken** @param clientId 客户端 ID* @return boolean* @author LiKe* @date 2020-07-09 10:02:11*/private boolean isSupportRefreshToken(String clientId) {return clientDetailsService.loadClientByClientId(clientId).getAuthorizedGrantTypes().contains("refresh_token");}/*** Create a refreshed authentication.<br>* <i>(Copied from DefaultTokenServices#createRefreshedAuthentication(OAuth2Authentication, TokenRequest))</i>** @param authentication The authentication.* @param tokenRequest   The scope for the refreshed token.* @return The refreshed authentication.* @throws InvalidScopeException If the scope requested is invalid or wider than the original scope.*/private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, TokenRequest tokenRequest) {Set<String> tokenRequestScope = tokenRequest.getScope();OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(tokenRequest);if (Objects.nonNull(tokenRequestScope) && !tokenRequestScope.isEmpty()) {Set<String> originalScope = clientAuth.getScope();if (Objects.isNull(originalScope) || !originalScope.containsAll(tokenRequestScope)) {throw new InvalidScopeException("Unable to narrow the scope of the client authentication to " + tokenRequestScope + ".", originalScope);} else {clientAuth = clientAuth.narrowScope(tokenRequestScope);}}return new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());}// =================================================================================================================/*** Description: 令牌生成器** @author LiKe* @date 2020-07-08 18:36:41*/private final class TokenGenerator {/*** Description: 创建 refresh-token<br>*     Details: 如果采用 JwtTokenStore, OAuth2RefreshToken 最终会在 JWtAccessTokenConverter 中被包装成用私钥加密后的以 OAuth2AccessToken 作为 payload 的 JWT 格式** @param authentication {@link OAuth2Authentication}* @return org.springframework.security.oauth2.common.OAuth2RefreshToken* @author LiKe* @date 2020-07-09 15:52:28*/public OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {if (!isSupportRefreshToken(authentication)) {return null;}final int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());final String tokenValue = UUID.randomUUID().toString();if (validitySeconds > 0) {return new DefaultExpiringOAuth2RefreshToken(tokenValue, new Date(System.currentTimeMillis() + validitySeconds * 1000L));}// 返回不过期的 refresh-tokenreturn new DefaultOAuth2RefreshToken(tokenValue);}/*** Description: 创建 access-token<br>*     Details: 如果采用 JwtTokenStore, OAuth2AccessToken 最终会在 JWtAccessTokenConverter 中被包装成用私钥加密后的以 OAuth2AccessToken 作为 payload 的 JWT 格式** @param authentication {@link OAuth2Authentication}* @param refreshToken   {@link OAuth2RefreshToken}* @return org.springframework.security.oauth2.common.OAuth2AccessToken* @author LiKe* @date 2020-07-09 15:51:29*/public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {final String tokenValue = UUID.randomUUID().toString();final CustomTokenGranter.CustomOAuth2AccessToken accessToken = new CustomTokenGranter.CustomOAuth2AccessToken(tokenValue);final int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());if (validitySeconds > 0) {accessToken.setExpiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L));}accessToken.setRefreshToken(refreshToken);accessToken.setScope(authentication.getOAuth2Request().getScope());return accessTokenEnhancer.enhance(accessToken, authentication);}}// =================================================================================================================public void setAuthenticationManager(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;}
}

自定义 TokenStore

TokenStore 是一个提供持久化 OAuth2 Token 的接口. 因为我们要采用 JWT 的形式作为 access-token, 所以重点关注它的实现类之一: org.springframework.security.oauth2.provider.token.store.JwtTokenStore:

概述:

JwtTokenStoreTokenStore 的其中之一实现. 默认实现是从令牌本身读取数据, 而不会进行持久化. 它本身需要 JwtAccessTokenConverter (extends TokenEnhancer) 来将常规令牌转换成 JWT 令牌, 并且如果 JwtAccessTokenConverter 设置了 keyPair (加密算法必须是 RSA), JwtAccessTokenConverter 就会对 access-token 和 refresh-token 用私钥加密, 公钥解密 (org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter#enhance(OAuth2AccessToken, OAuth2Authentication)).

先来看看 TokenStore 的接口定义:

// 从 OAuth2AccessToken 中读取 OAuth2Authentication. 如果不存在则返回 null.
OAuth2Authentication readAuthentication(OAuth2AccessToken token);// 从 OAuth2AccessToken#getValue() 中读取 OAuth2Authentication. 如果不存在则返回 null.
OAuth2Authentication readAuthentication(String token);// 保存 AccessToken. 参数是 OAuth2AccessToken 和与之关键的 OAuth2Authentication
// ☞ JwtTokenStore 并未实现
void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);// 通过 OAuth2AccessToken#getValue() 从存储系统中读取 OAuth2AccessToken
OAuth2AccessToken readAccessToken(String tokenValue);// 删除 OAuth2AccessToken
// ☞ JwtTokenStore 并未实现
void removeAccessToken(OAuth2AccessToken token);// 保存 RefreshToken. 参数是 OAuth2RefreshToken 和与之关联的 OAuth2Authentication
// ☞ JwtTokenStore 并未实现
void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);// 通过 OAuth2RefreshToken#getValue() 读取 OAuth2RefreshToken
OAuth2RefreshToken readRefreshToken(String tokenValue);// 从 OAuth2RefreshToken 中读取 OAuth2Authentication
OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);// 删除 OAuth2RefreshToken
void removeRefreshToken(OAuth2RefreshToken token);// 用 OAuth2RefreshToken 删除 OAuth2AccessToken (该功能能避免 RefreshToken 无限制的创建 AccessToken)
// ☞ JwtTokenStore 并未实现
void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);// 从 OAuth2Authentication 中获取 OAuth2AccessToken. 如果没有就返回 null
// ☞ JwtTokenStore 并未实现, 始终返回 null
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);// 通过 客户端ID 和 用户名 查询到与之关联的 OAuth2AccessToken.
// ☞ JwtTokenStore 并未实现, 始终返回空集合
Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);// 通过 客户端ID 查询到与之关联的 OAuth2AccessToken.
// ☞ JwtTokenStore 并未实现, 始终返回空集合
Collection<OAuth2AccessToken> findTokensByClientId(String clientId);

JwtTokenStore 显示依赖一个名为 JwtAccessTokenConverterTokenEnhancer:

public class JwtTokenStore implements TokenStore {private final JwtAccessTokenConverter jwtTokenEnhancer;// ...public JwtTokenStore(JwtAccessTokenConverter jwtTokenEnhancer) {this.jwtTokenEnhancer = jwtTokenEnhancer;}// ...
}

JwtAccessTokenConverter (TokenEnhancer & AccessTokenConverter)

JwtAccessTokenConverter 本质上是一个 TokenEnhancer, 后者只有一个 enhance 方法: 用于在 AccessToken 构建之前进行一些自定义的操作, 在 JwtAccessTokenConverter 中, 被用于把 OAuth2AccessToken 的 AccessToken 和 RefreshToken 包装成用 JWT 的形式并用服务端私钥加密 (具体载荷结构参考 DefayktAccessTokenConcerter#convertAccessToken(OAuth2AccessToken, OAuth2Authentication)).

同时也实现了接口 AccessTokenConverter, 后者作为给 Token Service 的实现提供的转换接口, 提供了 3 个接口方法:

// 将 OAuth2AccessToken 和 OAuth2Authentication 转换成 Map<String, ?>
Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);// 从 OAuth2AcccessToken#getValue() 和 信息 Map 中抽取 OAuth2AccessToken
OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map);// 通过从 AccessToken 中解码的信息 Map 抽取代表着 客户端 和 用户 (如果有) 的认证对象
OAuth2Authentication extractAuthentication(Map<String, ?> map);

JwtTokenStore 中有好几处都依赖了 JwtAccessTokenConverter:

public class JwtTokenStore implements TokenStore {// ...// 将 JWT 格式的令牌的载荷转换成 Map 并从中抽取成认证对象 OAuth2Authentication@Overridepublic OAuth2Authentication readAuthentication(String token) {return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token));}// 1. 从 OAuth2AccessToken#getValue() 中抽取认证对象, 并联合 OAuth2AccessToken#getValue() 组装成 OAuth2AccessToken// 2. 判断当前 OAuth2AccessToken 是否是一个 RefreshToken (根据其信息 Map 中是否包含键为 ati 的记录)@Overridepublic OAuth2AccessToken readAccessToken(String tokenValue) {OAuth2AccessToken accessToken = convertAccessToken(tokenValue);if (jwtTokenEnhancer.isRefreshToken(accessToken)) {throw new InvalidTokenException("Encoded token is a refresh token");}return accessToken;}private OAuth2AccessToken convertAccessToken(String tokenValue) {return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));}// 在从 tokenValue 中读取 OAuth2RefreshToken 的方法 OAuth2RefreshToken readRefreshToken(String tokenValue) 中, 同样调用了 JwtTokenEnhancer#isRefreshToken(OAuth2AccessToken) 用于判断是否是 RefreshToken.private OAuth2RefreshToken createRefreshToken(OAuth2AccessToken encodedRefreshToken) {if (!jwtTokenEnhancer.isRefreshToken(encodedRefreshToken)) {throw new InvalidTokenException("Encoded token is not a refresh token");}if (encodedRefreshToken.getExpiration()!=null) {return new DefaultExpiringOAuth2RefreshToken(encodedRefreshToken.getValue(),encodedRefreshToken.getExpiration());         }return new DefaultOAuth2RefreshToken(encodedRefreshToken.getValue());}// ...}

签名与校验

对于 JWT 来说, 我们知道它本身是 Header, Payload 和 Signature 三部分以 . 拼接而成的字符串. 其中 Signature 是对前两部分的签名, 用于防止数据被篡改.

Reference: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

JwtAccessTokenConverter 的源代码中有几个比较关键的成员变量, 它们分别是:

private String verifierKey = new RandomValueStringGenerator().generate();private Signer signer = new MacSigner(verifierKey);private String signingKey = verifierKey;private SignatureVerifier verifier;

先说说 verifierKey 和 verifier, 默认值是一个 6 位的随机字符串 (new RandomValueStringGenerator().generate()), 和它 “配套” 的 verifier 是持有这个 verifierKey 的 MacSigner (用 HMACSHA256 算法验证 Signature) / RsaVerifier (用 RSA 公钥验证 Signature), 在 JwtAccessTokenConverter 的 decode 方法中作为签名校验器被传入 JwtHelper 的 decodeAndVerify(@NotNull String token, org.springframework.security.jwt.crypto.sign.SignatureVerifier verifier):

protected Map<String, Object> decode(String token) {try {Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);String content = jwt.getClaims();Map<String, Object> map = objectMapper.parseMap(content);if (map.containsKey(EXP) && map.get(EXP) instanceof Integer) {Integer intValue = (Integer) map.get(EXP);map.put(EXP, new Long(intValue));}return map;}catch (Exception e) {throw new InvalidTokenException("Cannot convert access token to JSON", e);}
}

而在 JwtHelper 的目标方法中, 首先把 token 的三个部分 (以 . 分隔的) 拆分出来, Base64.urlDecode 解码. 再用我们传入的 verifier 将 “Header.Payload” 编码 (如果是 RSA, 就是公钥.) 并与拆分出来的 Signature 部分比对 (Reference: org.springframework.security.jwt.crypto.sign.RsaVerifier#verify).

对应的, signer 和 signingKey 作为签名 “组件” 存在, (可以看到在默认情况下, JwtAccessTokenConverter 对 JWT 的 Signature 采用的是对称加密, signingKey 和 verifierKey 一致) 在 JwtHelper 的 encode(@NotNull CharSequence content, @NotNull org.springframework.security.jwt.crypto.sign.Signer signer) 方法中, 被用于将 “Header.Payload” 加密 (如果是 RSA, 就是私钥) (Reference: org.springframework.security.jwt.crypto.sign.RsaSigner#sign).

所以算法本质上不是对 JWT 整体进行加解密, 而是对其中的 Signature 部分

当然, 用户也可以通过 JwtAccessTokenConverter 提供的 setKeyPair(KeyPair) 自定义 RSA 的密钥对. 可以显示传入公私钥对, signer 持有私钥, verifier 持有公钥.

public void setKeyPair(KeyPair keyPair) {PrivateKey privateKey = keyPair.getPrivate();Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");signer = new RsaSigner((RSAPrivateKey) privateKey);RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();verifier = new RsaVerifier(publicKey);verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))+ "\n-----END PUBLIC KEY-----";
}

(这个方法也是我们要用到的)


۞ 大致总结一下:

  • 无论是默认的 DefaultTokenServices 还是我们自定义的 AuthorizationServerTokenServices, 在 createAccessToken 末尾都显示调用了 tokenEnhancer 来自定义令牌.

  • JwtTokenStore (impements TokenStore) 提供了操作 JWT 形式令牌的接口, 具体实现里, 它借助 JwtAccessTokenConverter 将包装和抽取令牌.

  • JwtAccessTokenConverter 本身实现了TokenEnhancerAccessTokenConverter 两个接口, 分别提供了包装令牌的方法实现, 和抽取令牌的方法实现.

代码实现 - JwtTokenStore

我们首先需要生成密钥对 (KeyPair) 和 KeyStore, 这里我们采用 PKCS#12 类型的密钥库, 与 JKS 类型的区别以及相关说明, 请查阅:

  • KeyStore 简述

  • Keytool 简述

  • Certificate Chain (证书链) 简述

keytool -genkeypair -alias authorization-server-jwt-keypair -keyalg RSA -keysize 2048 -dname "CN=caplike, OU=personal, O=caplike, L=Chengdu, ST=Sichuan, C=CN" -validity 3650 -storetype JKS -keystore authorization-server.jks -storepass ********

执行如下命令从密钥库中导出公钥和证书的 PEM 格式:

keytool -list -rfc --keystore authorization-server.jks | openssl x509 -inform pem -pubkey
输入密钥库口令:  ********
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlLx5bz3zu/ptZpVuvCBQ
Z4dMeDhmZJmyxia7A9706B5o/ipLFcZnjOtKVQcZTa8UOniTDJ46DmMyK2Q5oW8d
24cpMdPSwxNMU/7dOv40DFnoFUFIWUR/+fAZVTCfJb7pBpzWpmLmvOhLV8rSOKbJ
TIeRUWgsFZsCJJaqIa3/6k7moTV4DURUgh1ABmMyXUd3/zeSkdPJXu9QCdxFygSP
VJs4d5Bqr97mROIdt9qmngap1Lch2elwrzWuQx63mGxoK+lxEQB6ftdPLvpEABuC
Bs7hO18CBj5ei9G+foaFe/77muNCILAtvc8UiD6PRbf5e1YXEp0IHZisuOhedjqB
FQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDbzCCAlegAwIBAgIEAfMOsjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJD
TjEQMA4GA1UECBMHU2ljaHVhbjEQMA4GA1UEBxMHQ2hlbmdkdTEQMA4GA1UEChMH
Y2FwbGlrZTERMA8GA1UECxMIcGVyc29uYWwxEDAOBgNVBAMTB2NhcGxpa2UwHhcN
MjAwNzE3MDc0MzU0WhcNMzAwNzE1MDc0MzU0WjBoMQswCQYDVQQGEwJDTjEQMA4G
A1UECBMHU2ljaHVhbjEQMA4GA1UEBxMHQ2hlbmdkdTEQMA4GA1UEChMHY2FwbGlr
ZTERMA8GA1UECxMIcGVyc29uYWwxEDAOBgNVBAMTB2NhcGxpa2UwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUvHlvPfO7+m1mlW68IFBnh0x4OGZkmbLG
JrsD3vToHmj+KksVxmeM60pVBxlNrxQ6eJMMnjoOYzIrZDmhbx3bhykx09LDE0xT
/t06/jQMWegVQUhZRH/58BlVMJ8lvukGnNamYua86EtXytI4pslMh5FRaCwVmwIk
lqohrf/qTuahNXgNRFSCHUAGYzJdR3f/N5KR08le71AJ3EXKBI9Umzh3kGqv3uZE
4h232qaeBqnUtyHZ6XCvNa5DHreYbGgr6XERAHp+108u+kQAG4IGzuE7XwIGPl6L
0b5+hoV7/vua40IgsC29zxSIPo9Ft/l7VhcSnQgdmKy46F52OoEVAgMBAAGjITAf
MB0GA1UdDgQWBBRqowFVjNkW77ZciS10KyMWs/3n2jANBgkqhkiG9w0BAQsFAAOC
AQEAJ+d+/0ss/Hl8IhPuIbH5Hh3MMxK8f02/QBPyJ5+ZJgt9k1BZc6/eMYbWd41z
05gb2m2arXfAS2HEdsY1pCfcssb85cVYUwMoDfK7pLRX34V0uhdUm0wqTBumIs2i
CCLCz7Eci4XpAv+RWHVKXbg+pP7GrKBh0iNYTuV+pDr+D7K6rZwGjYsGAqqpc1Lj
NNaN68pHhTnwXu4igM/gLsNRmR+2zXyJ1FZegnk0fsFWojOqHwCZxYli9245N4Hg
ePIVTvFTu+QzdLzFUcsGqhrynHfwQOvTyPMpaowpOsguNSzTdmRRK3QdtKHglE10
us40NUJZQgavCigGcVwAv/jCdA==
-----END CERTIFICATE-----

或是直接导出证书:

keytool -exportcert -alias authorization-server-jwt-keypair -storetype PKCS12 -keystore authorization-server.jks -file public.cert -storepass ************
存储在文件 <public.cert> 中的证书.

Reference: 从证书中读取公钥

分析就到这里, 下面我们为 AuthorizationServerEndpointsConfigurer 指定 tokenStore 和 tokenEnhancer:

/*** 授权服务器配置类<br>* {@code @EnableAuthorizationServer} 会启用 {@link org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint}* 和 {@link org.springframework.security.oauth2.provider.endpoint.TokenEndpoint} 端点.** @author LiKe* @version 1.0.0* @date 2020-06-15 09:43* @see AuthorizationServerConfigurerAdapter*/
@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {//.../*** Description: 配置 {@link AuthorizationServerEndpointsConfigurer}<br>* Details: 配置授权服务器端点的非安全性特性, 例如 令牌存储, 自定义. 如果是密码授权, 需要在这里提供一个 {@link AuthenticationManager}** @see AuthorizationServerConfigurerAdapter#configure(AuthorizationServerEndpointsConfigurer)*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {// @formatter:off// 对于密码授权模式, 需要提供 AuthenticationManager 用于用户信息的认证endpoints.authenticationManager(authenticationManager)// ~ 自定义的 WebResponseExceptionTranslator, 默认使用 DefaultWebResponseExceptionTranslator, 在 /oauth/token 端点//   ref: TokenEndpoint.exceptionTranslator(webResponseExceptionTranslator)// ~ 自定义的 TokenGranter.tokenGranter(new CustomTokenGranter(endpoints, authenticationManager))// ~ 自定义的 TokenStore.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter())// ~ 自定义的 AuthorizationServerTokenServices.tokenServices(new CustomAuthorizationServerTokenServices(endpoints))// ~ refresh_token required.userDetailsService(userDetailsService);// @formatter:on}/*** Description: 自定义 {@link JwtTokenStore}** @return org.springframework.security.oauth2.provider.token.TokenStore {@link JwtTokenStore}* @author LiKe* @date 2020-07-20 18:11:25*/private TokenStore tokenStore() {return new JwtTokenStore(jwtAccessTokenConverter());}/*** Description: 为 {@link JwtTokenStore} 所须** @return org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter* @author LiKe* @date 2020-07-20 18:04:48*/private JwtAccessTokenConverter jwtAccessTokenConverter() {final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("authorization-server.jks"), "********".toCharArray());final JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("authorization-server-jwt-keypair"));return jwtAccessTokenConverter;}//...}

真正代码层面就这么点改动, 好了, 接下来我们启动服务器, 以密码授权形式请求授权服务器, 得到响应:

{"status": 200,"timestamp": "2020-07-21 15:51:58","message": "OK","data": "{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sImV4cCI6MTU5NTM2MTExOCwidXNlcl9uYW1lIjoiY2FwbGlrZSIsImp0aSI6ImRkZmExMTgwLTE0MDAtNDA0MC1iNjU3LTAzMTJmMWQ1OGIwNyIsImNsaWVudF9pZCI6ImNsaWVudC1hIiwic2NvcGUiOlsiQUNDRVNTX1JFU09VUkNFIl19.XHTbHaZnpudapYmKxx2RDwiaV71h0GvG61Dtgbc5VYTPN3xBoA1n6Ws8uSHd0tUFM-dpbqDOzL4RUNrXs-baTwVpTvBxtjNUdRh0fp3Vc3aMnWxkyQVivDVU_ZbDTSoqUrsJOBanNYH-V89jWP1H-V5bNUQK2EWWnz6xVWRHIcAMUJhW8ZC-rekcVk-v5wA4CJH9XFvkNbOsGOLIUYNVXGY27LhlGKWuXf1_EX-6kTMp7fKFwBlrjuujBn2NpRvzKxTyfW5O8czG-7hPDCumpfOlrTYlCOzTXc5Xr7hNUMZYfIurV6WtU5A__-nvQYRt3HLO48OXlsgAWn7e8NfrCg\",\"token_type\":\"bearer\",\"refresh_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sInVzZXJfbmFtZSI6ImNhcGxpa2UiLCJzY29wZSI6WyJBQ0NFU1NfUkVTT1VSQ0UiXSwiYXRpIjoiZGRmYTExODAtMTQwMC00MDQwLWI2NTctMDMxMmYxZDU4YjA3IiwiZXhwIjoxNTk3OTA5OTE4LCJqdGkiOiJiMTFjOGZkZi1lYzI4LTRmNWEtYjY0Ni1hZWVmNTJlNTQ4NDEiLCJjbGllbnRfaWQiOiJjbGllbnQtYSJ9.C-PMeXPLSDxBTpZE3m3dplAXF0BTV3OSOcRuOTTnZEvXStOLOfk7_SgTLetkzaZkoOO9pon7ezgceiFNOekHPM3SbNIgLpUKaXA3jrU3lYvuYqfqDjKHsL08wlzeCqdZL2vYpo_b7aRkKqEcar8_qEwEZBG9jVZVkZSLtAmwxW4HruPNe04EmbZiJsBT1NCGdAvWBbiHJ18ltZZROZWDILc7If9RCVp3U9AY5xAzE4BqIsZQ3zFiOv5RldfkJHYLmvlA0IjYbUSoSoeLqym_5YOWaAvTz1u0izAkXSScRwe5vfwJjwMr_0pXX6eACz1E4vPFRGdeOy_0iyyk17zT0Q\",\"expires_in\":43199,\"scope\":\"ACCESS_RESOURCE\",\"jti\":\"ddfa1180-1400-4040-b657-0312f1d58b07\"}"
}

(为什么响应是这种结构? 本文的代码是以 上一篇 为基础构建, 已经具备了统一响应格式的特性). 其中, data 为 CustomTokenGranter.CustomOAuth2AccessToken 序列化的结果:

{"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sImV4cCI6MTU5NTM2MTExOCwidXNlcl9uYW1lIjoiY2FwbGlrZSIsImp0aSI6ImRkZmExMTgwLTE0MDAtNDA0MC1iNjU3LTAzMTJmMWQ1OGIwNyIsImNsaWVudF9pZCI6ImNsaWVudC1hIiwic2NvcGUiOlsiQUNDRVNTX1JFU09VUkNFIl19.XHTbHaZnpudapYmKxx2RDwiaV71h0GvG61Dtgbc5VYTPN3xBoA1n6Ws8uSHd0tUFM-dpbqDOzL4RUNrXs-baTwVpTvBxtjNUdRh0fp3Vc3aMnWxkyQVivDVU_ZbDTSoqUrsJOBanNYH-V89jWP1H-V5bNUQK2EWWnz6xVWRHIcAMUJhW8ZC-rekcVk-v5wA4CJH9XFvkNbOsGOLIUYNVXGY27LhlGKWuXf1_EX-6kTMp7fKFwBlrjuujBn2NpRvzKxTyfW5O8czG-7hPDCumpfOlrTYlCOzTXc5Xr7hNUMZYfIurV6WtU5A__-nvQYRt3HLO48OXlsgAWn7e8NfrCg","token_type": "bearer","refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sInVzZXJfbmFtZSI6ImNhcGxpa2UiLCJzY29wZSI6WyJBQ0NFU1NfUkVTT1VSQ0UiXSwiYXRpIjoiZGRmYTExODAtMTQwMC00MDQwLWI2NTctMDMxMmYxZDU4YjA3IiwiZXhwIjoxNTk3OTA5OTE4LCJqdGkiOiJiMTFjOGZkZi1lYzI4LTRmNWEtYjY0Ni1hZWVmNTJlNTQ4NDEiLCJjbGllbnRfaWQiOiJjbGllbnQtYSJ9.C-PMeXPLSDxBTpZE3m3dplAXF0BTV3OSOcRuOTTnZEvXStOLOfk7_SgTLetkzaZkoOO9pon7ezgceiFNOekHPM3SbNIgLpUKaXA3jrU3lYvuYqfqDjKHsL08wlzeCqdZL2vYpo_b7aRkKqEcar8_qEwEZBG9jVZVkZSLtAmwxW4HruPNe04EmbZiJsBT1NCGdAvWBbiHJ18ltZZROZWDILc7If9RCVp3U9AY5xAzE4BqIsZQ3zFiOv5RldfkJHYLmvlA0IjYbUSoSoeLqym_5YOWaAvTz1u0izAkXSScRwe5vfwJjwMr_0pXX6eACz1E4vPFRGdeOy_0iyyk17zT0Q","expires_in": 43199,"scope": "ACCESS_RESOURCE","jti": "ddfa1180-1400-4040-b657-0312f1d58b07"
}

对于 access_token, 我们分别把它的 Header, Payload, Signature 解码得到:

{"alg":"RS256","typ":"JWT"}.{"aud":["resource-server"],"exp":1595361118,"user_name":"caplike","jti":"ddfa1180-1400-4040-b657-0312f1d58b07","client_id":"client-a","scope":["ACCESS_RESOURCE"]}.XHTbHaZnpudapYmKxx2RDwiaV71h0GvG61Dtgbc5VYTPN3xBoA1n6Ws8uSHd0tUFM-dpbqDOzL4RUNrXs-baTwVpTvBxtjNUdRh0fp3Vc3aMnWxkyQVivDVU_ZbDTSoqUrsJOBanNYH-V89jWP1H-V5bNUQK2EWWnz6xVWRHIcAMUJhW8ZC-rekcVk-v5wA4CJH9XFvkNbOsGOLIUYNVXGY27LhlGKWuXf1_EX-6kTMp7fKFwBlrjuujBn2NpRvzKxTyfW5O8czG-7hPDCumpfOlrTYlCOzTXc5Xr7hNUMZYfIurV6WtU5A__-nvQYRt3HLO48OXlsgAWn7e8NfrCg

而 refresh_token:

{"alg":"RS256","typ":"JWT"}.{"aud":["resource-server"],"user_name":"caplike","scope":["ACCESS_RESOURCE"],"ati":"ddfa1180-1400-4040-b657-0312f1d58b07","exp":1597909918,"jti":"b11c8fdf-ec28-4f5a-b646-aeef52e54841","client_id":"client-a"}.C-PMeXPLSDxBTpZE3m3dplAXF0BTV3OSOcRuOTTnZEvXStOLOfk7_SgTLetkzaZkoOO9pon7ezgceiFNOekHPM3SbNIgLpUKaXA3jrU3lYvuYqfqDjKHsL08wlzeCqdZL2vYpo_b7aRkKqEcar8_qEwEZBG9jVZVkZSLtAmwxW4HruPNe04EmbZiJsBT1NCGdAvWBbiHJ18ltZZROZWDILc7If9RCVp3U9AY5xAzE4BqIsZQ3zFiOv5RldfkJHYLmvlA0IjYbUSoSoeLqym_5YOWaAvTz1u0izAkXSScRwe5vfwJjwMr_0pXX6eACz1E4vPFRGdeOy_0iyyk17zT0Q

至此, 我们的自定义令牌应该算是初具规模了. 接下来, 还有几个细节需要 “打磨”, 请继续往下看…

ResourceServer

接下来我们编写资源服务器: 让资源服务器请求远端授权服务器的 CheckTokenEndpoint 端点, 验证签名并解析 JWT.

引言

ResourceServerTokenServices

ResourceServerTokenServices 默认有两个实现, 一个是 RemoteTokenServices, 另外一个是 DefaultTokenServices.

  • 在资源服务器, 我们使用的 RemoteTokenServices 来像授权服务器发起检查并解析令牌的请求, 并用其结果封装成资源服务器的 OAuth2Authentication;

接下来我们调整资源服务器, 主要涉及的方面有:

  1. 通过 RemoteTokenServices 请求授权服务器解析令牌.
  2. 资源服务器响应格式一致性.

调整 AuthorizationServer 的 CustomAuthorizationServerTokenServices

之前我们的 CustomAuthorizationServerTokenServices 只重写了 AuthorizationServerTokenServices 的接口, 而现在由于资源服务器采用 RemoteTokenServices 向授权服务器请求解析令牌, 所以 CustomAuthorizationServerTokenServices 也需要"承担" ResourceServerTokenServices 的职责, 反映到代码上, 我们需要在 CustomAuthorizationServerTokenServices 实现如下 2 个方法:

public class CustomAuthorizationServerTokenServices extends DefaultTokenServices {// ...// ~ Methods implementing from ResourceServerTokenServices//   当资源服务器的 ResourceServerTokenServices 是 RemoteTokenServices 的时候 (在 CheckTokenEndpoint 被请求的时候会调用)// =================================================================================================================@Overridepublic OAuth2AccessToken readAccessToken(String accessToken) {return tokenStore.readAccessToken(accessToken);}@Overridepublic OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {final OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);if (Objects.isNull(accessToken)) {throw new InvalidTokenException("无效的 access_token: " + accessTokenValue);} else if (accessToken.isExpired()) {tokenStore.removeAccessToken(accessToken);throw new InvalidTokenException("无效的 access_token (已过期): " + accessTokenValue);}final OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);if (Objects.isNull(oAuth2Authentication)) {throw new InvalidTokenException("无效的 access_token: " + accessTokenValue);}final String clientId = oAuth2Authentication.getOAuth2Request().getClientId();try {clientDetailsService.loadClientByClientId(clientId);} catch (ClientRegistrationException e) {throw new InvalidTokenException("无效的客户端: " + clientId, e);}return oAuth2Authentication;}// ...}

这样, 无论是其他应用直接请求授权服务器申请 / 续期令牌, 还是资源服务器请求授权服务器解析令牌, 我们的授权服务器都有能力处理了.

自定义 ResourceServer 的响应格式 - ResourceServerConfiguration

在 上一篇 文章中, 我们已经规范并自定义了授权服务器的响应格式.

本篇我们将自定义资源服务器的响应格式 - 与授权服务器一致.

为什么需要: 当前如果资源服务器携带过期或是无效的令牌请求授权服务器, 后者返回的是自定义的响应格式, 但是响应回到资源服务器的时候, 信息并没有正确的返回给前端 (默认处理是被异常包装并抛给上层了, 最终会导致跳转到默认的错误页 /error).

综上所述, 我们需要阻止这一过程, 并从其中某一个恰当的位置, “织入” 我们自己的处理逻辑.

首先通过 RemoteTokenServices 的源代码发现其内部使用了 RestTemplate 来调用远端服务, 而 RestTemplate 本身可以指定一个 errorHandler, 用于处理调用远端 /oauth/check_token 端点 (CheckTokenEndpoint) 的非正常响应. 这个 errorHandler 默认是调用超类 (DefaultResponseErrorHandler) 的 handleError 方法. 上面也说到了, 我们需要"接管"这一过程.

通过断点跟踪我们看到用户定义的 RemoteTokenServices会在 OAuthenticationProcessingFilter 的 doFilter 中, 由 AuthenticationManager.authenticate 调用. 其中 AuthenticationManager 的真实类型是 OAuth2AuthenticationManager, 其 authenticate 方法会调用 tokenServices (当前场景下, 就是我们定义的 RemoteTokenServices) 的 loadAuthentication, 而如果这个 tokenServices 的真实类型是
RemoteTokenServices, 则会触发资源服务器去请求授权服务器的 /oauth/check_token 端点解析令牌的操作. 所以在这一步, 如果令牌过期或是无效, 授权服务器的响应会传回给资源服务器, 如何处理这个响应, 就是我们这里需要考虑的内容.

由于整个调用链的上层是 OAuth2AuthenticationProcessingFilter, 通过查看源码我们知道, 如果认证过程中抛出 OAuth2Exception, 会被 AuthenticationEntryPoint 处理. 我的方案是获取 response 的 body 数据, 显示抛出 OAuth2Exception, 最终把请求交由 AuthenticationEntryPoint 处理.

下面来看代码:

/*** 资源服务器配置** @author LiKe* @version 1.0.0* @date 2020-06-13 20:55*/
@Slf4j
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {private static final String RESOURCE_ID = "resource-server";private static final String AUTHORIZATION_SERVER_CHECK_TOKEN_ENDPOINT_URL = "http://localhost:18957/token-customize-authorization-server/oauth/check_token";// =================================================================================================================private AuthenticationEntryPoint authenticationEntryPoint;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {// @formatter:offresources.resourceId(RESOURCE_ID).tokenServices(remoteTokenServices()).stateless(true);resources.authenticationEntryPoint(authenticationEntryPoint);// @formatter:on}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated();}// -----------------------------------------------------------------------------------------------------------------/*** Description: 远端令牌服务类<br>* Details: 调用授权服务器的 /oauth/check_token 端点解析令牌. <br>* 在本 DEMO 中, 调用授权服务器的 {@link org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint} 端点, <br>* 将私钥签名的 JWT 发到授权服务器, 后者用公钥验证 Signature 部分** @return org.springframework.security.oauth2.provider.token.RemoteTokenServices* @author LiKe* @date 2020-07-22 20:33:13*/private RemoteTokenServices remoteTokenServices() {final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();// ~ 设置 RestTemplate, 以自行决定异常处理final RestTemplate restTemplate = new RestTemplate();restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Override// Ignore 400public void handleError(ClientHttpResponse response) throws IOException {final int rawStatusCode = response.getRawStatusCode();System.out.println(rawStatusCode);if (rawStatusCode != 400) {final String responseData = new String(super.getResponseBody(response));throw new OAuth2Exception(responseData);}}});remoteTokenServices.setRestTemplate(restTemplate);// ~ clientId 和 clientSecret 会以 base64(clientId:clientSecret) basic 方式请求授权服务器remoteTokenServices.setClientId(RESOURCE_ID);remoteTokenServices.setClientSecret("resource-server-p");// ~ 请求授权服务器的 CheckTokenEndpoint 端点解析 JWT (AuthorizationServerEndpointsConfigurer 中指定的 tokenServices.//   实现了 ResourceServerTokenServices 接口,//   如果没有, 则使用默认的 (org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration.checkTokenEndpoint)remoteTokenServices.setCheckTokenEndpointUrl(AUTHORIZATION_SERVER_CHECK_TOKEN_ENDPOINT_URL);return remoteTokenServices;}// -----------------------------------------------------------------------------------------------------------------@Autowiredpublic void setAuthenticationEntryPoint(@Qualifier("customAuthenticationEntryPoint") AuthenticationEntryPoint authenticationEntryPoint) {this.authenticationEntryPoint = authenticationEntryPoint;}
}

为了达到这个目的, 当然的我们在资源服务器端也需要自定义 AuthenticationEntryPoint:
(由于授权服务器返回的格式已经是 SecurityResponse 序列化的 (我们期望的) 标准结构. 所以这里, 我们只需要读取其内容即可. 譬如授权服务器返回的响应码, 也正是资源服务器要返向前端的响应码)

/*** 自定义的 {@link AuthenticationEntryPoint}** @author LiKe* @version 1.0.0* @date 2020-07-23 15:29*/
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {log.debug("Custom AuthenticationEntryPoint triggered with exception: {}.", authException.getClass().getCanonicalName());// 原始异常信息final String authExceptionMessage = authException.getMessage();try {final SecurityResponse securityResponse = JSON.parseObject(authExceptionMessage, SecurityResponse.class);ResponseWrapper.wrapResponse(response, securityResponse);} catch (JSONException ignored) {ResponseWrapper.forbiddenResponse(response, authExceptionMessage);}}}

启动授权服务器和资源服务器, 当资源服务器以过期的令牌请求授权服务器时, 可以看到返回的正式我们期望的响应格式:

{"timestamp": "2020-07-23 17:28:18","status": 403,"message": "Cannot convert access token to JSON","data": "{}"
}

后记

但是这种在资源服务器通过使用 RemoteTokenServices 与授权服务器频繁交互的弊端也很明显, 每个携带令牌的请求都会与授权服务器交互一次: 授权服务器的压力过大, 设想我们有 N 个后端服务, 这带来的性能问题是不可忽视的. 下一篇, 我们将讨论如何 “解耦”, 让资源服务器 “自治”.


P.S.

本文是在 上一篇 的基础上做的扩展, 重复的部分没有赘述.

☞ 代码清参考: token-customize-resource-server-remote-token-services & token-customize-authorization-server

SpringSecurity OAuth2 (7) 自定义 AccessToken 和 RefreshToken (JWT with RSA 签名)相关推荐

  1. SpringSecurity Oauth2 - 自定义 SpringBoot Starter 远程访问受限资源

    文章目录 1. 自定义 SpringBoot Starter 1. 统一的dependency管理 2. 对外暴露 properties 3. 实现自动装配 4. 指定自动配置类的路径 META-IN ...

  2. springsecurity oauth2使用jwt实现单点登录

    Jwt方式已经分享在文章结尾处的百度网盘链接中,redis方式可以看我以前发表的文章. 文章目录 前言 一.springsecurity oauth2 + redis方式的缺点 二.oauth2认证的 ...

  3. 权限管理SpringSecurity Oauth2整合JWT实战总结(三)

    1.JWT 1.1.基本的认证机制 1) HTTP Basic Auth HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic ...

  4. SpringSecurity+OAuth2.0+JWT实现单点登录应用

    SpringSecurity+OAuth2.0+JWT实现单点登录应用 gitee项目练习地址:https://gitee.com/xzq25_com/springsecurity.oauth2 OA ...

  5. 整合SpringSecurity OAuth2 JWT (附原因)一步步来如此简单

    梳理整合 SpringCloud 和 SpringSecurity OAuth2 的搭建流程,网上看了一些,感觉都很差强人意,所以决定梳理下,不多说了开鲁吧. 首先要搭建微服务基础加包版本,基于阿里系 ...

  6. OAuth2.0 - 自定义模式授权 - 短信验证码登录

    一.OAuth2.0 - 自定义模式授权 上篇文章我们分析了目前的情况,演示了微服务的大环境下在保证安全的情况下通过SpringGateWay实现统一的鉴权处理,但是前面的演示中,我们都是基于用户名密 ...

  7. 零基础学习SpringSecurity OAuth2 四种授权模式(理论+实战)(配套视频讲解)

    配套视频直达 背景 前段时间有同学私信我,让我讲下Oauth2授权模式,并且还强调是零基础的那种,我也不太理解这个零基础到底是什么程度,但是我觉得任何阶段的同学看完我这个视频,对OAuth2的理解将会 ...

  8. 六、SpringSecurity OAuth2 + SpringCloud Gateway实现统一鉴权管理

    代码 代码仓库:地址 代码分支:lesson6 博客:地址 简介 在先前文章中,我们使用SpringSecurity OAuth2搭建了一套基于OAuth2协议的授权系统,并扩展了手机验证码授权模式. ...

  9. SpringSecurity OAuth2异常处理OAuth2Exception

    前言 在我们使用SpringSecurity OAuth2做认证授权时,默认返回都是SpringSecurity OAuth2提供好的,返回不是很友好,本章就是针对这些异常做统一返回处理,主要解决返回 ...

最新文章

  1. 表单提交时有的字段可以传递到后台有的不可以
  2. [Eclispe] NDK内建include路径修改
  3. input type=file 实现上传、预览、删除等功能
  4. 如何在JavaScript中大写字符串的首字母
  5. BitMEX联合创始人:以比特币为首的加密货币综合体是防范恶性通货膨胀的最佳对冲
  6. springboot-web进阶(三)——统一异常处理
  7. 【小技巧积累】用Style实现必填提示“*”根据选项的不同而显示或隐藏
  8. linux在路径下创建文件,从可以在Linux中打开的文件路径创建文件
  9. 工业企业外购材料进项税额的会计处理
  10. 客户端无法向springcloud注册中心注册服务,提示连接超时
  11. TCP四次挥手的等待时间为什么是2MSL而不是1MSL
  12. linux下好用的python编辑器_分享|Linux上几款好用的字幕编辑器
  13. 二十:让行内元素在div中垂直居中
  14. 安卓导入项目遇到“Sync Android SDKs”
  15. 阿里JAVA实习生面试总结(2019年春招)
  16. 快速入门 | 篇十四:运动控制器基础轴参数与基础运动控制指令
  17. Flutter Riverpod 全面深入解析,为什么官方推荐它?
  18. kali网络渗透实验一:网络扫描与网络侦查
  19. 计算机的显卡功能,电脑怎么看显卡参数 显卡有什么作用
  20. 口袋进化服务器维护,口袋进化平民攻略

热门文章

  1. Struts2框架中的Action接口和ActionSupport类
  2. Openwrt 开启openvpn服务访问内网
  3. PaddleSpeech TTS 设计要素 — 实验输出目录
  4. Mac安装配置Maven及镜像
  5. CSND近期推出的猿如意到底有没有必要安装
  6. antd 中的 table 组件设置 size 属性值为 small 后,表头背景色设置的解决方案
  7. 基于 Markdown 与 Git 的知识管理系统
  8. 深度学习图像识别:基础知识与环境搭建
  9. 北京交大计算机专业导师,北京交通大学教师名录
  10. python接入Vissim二次开发,源码