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

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)
     */
    @Override
    public 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)
        ));
    }

    @Override
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        log.debug("Custom TokenGranter :: grant token with type {}", grantType);

        // 如果发生异常, 会触发 WebResponseExceptionTranslator
        final 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
         */
        @SneakyThrows
        public 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);
        }

        @Override
        public 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)
     */
    @Override
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        // 当前客户端是否支持 refresh_token
        final boolean supportRefreshToken = isSupportRefreshToken(authentication);
        OAuth2RefreshToken existingRefreshToken = null;

        // 如果已经存在令牌
        final OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        if (Objects.nonNull(existingAccessToken)) {
            if (existingAccessToken.isExpired()) {
                // 如果已过期, 则删除 AccessToken 和 RefreshToken
                if (supportRefreshToken) {
                    existingRefreshToken = existingAccessToken.getRefreshToken();
                    tokenStore.removeRefreshToken(existingRefreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            } else {
                // 否则重新保存令牌 (以防 authentication 已经改变)
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        // 生成新的 refresh_token
        OAuth2RefreshToken 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_token
        final 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)
     */
    @Override
    public 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 认证信息, 并基于此构建新的 OAuth2AccessToken
        oAuth2Authentication = createRefreshedAuthentication(oAuth2Authentication, tokenRequest);
        // 获取新的 refresh_token
        final OAuth2AccessToken refreshedAccessToken = tokenGenerator.createAccessToken(oAuth2Authentication, refreshToken);
        tokenStore.storeAccessToken(refreshedAccessToken, oAuth2Authentication);
        return refreshedAccessToken;
    }

    @Override
    public 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-token
            return 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
    @Override
    public OAuth2Authentication readAuthentication(String token) {
        return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token));
    }
    
    // 1. 从 OAuth2AccessToken#getValue() 中抽取认证对象, 并联合 OAuth2AccessToken#getValue() 组装成 OAuth2AccessToken
    // 2. 判断当前 OAuth2AccessToken 是否是一个 RefreshToken (根据其信息 Map 中是否包含键为 ati 的记录)
    @Override
	public 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 类型的区别以及相关说明, 请查阅:

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)
     */
    @Override
    public 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 被请求的时候会调用)
    // =================================================================================================================

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        return tokenStore.readAccessToken(accessToken);
    }

    @Override
    public 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;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // @formatter:off
        resources.resourceId(RESOURCE_ID).tokenServices(remoteTokenServices()).stateless(true);

        resources.authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }

    @Override
    public 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 400
            public 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;
    }

    // -----------------------------------------------------------------------------------------------------------------

    @Autowired
    public 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 {

    @Override
    public 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

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值