自定义 SpringSecurity OAuth2 的 AccessToken 和 RefreshToken (采用 RSA 签名的 JWT)

访客 256 0

文章目录

  • 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@EnableAuthorizationServerpublic 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 接口的方法签名:

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

自定义 SpringSecurity OAuth2 的 AccessToken 和 RefreshToken (采用 RSA 签名的 JWT)-第1张图片-谷歌商店上架

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

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

TokenStore

针对 OAuth2 令牌的持久化的接口. Spring Security OAuth 2.0 提供了好几个开箱即用的实现.
自定义 SpringSecurity OAuth2 的 AccessToken 和 RefreshToken (采用 RSA 签名的 JWT)-第2张图片-谷歌商店上架
默认 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 是一个定义令牌颁发实现类的标准接口, 它有众多的实现类. 先来一瞥:
自定义 SpringSecurity OAuth2 的 AccessToken 和 RefreshToken (采用 RSA 签名的 JWT)-第3张图片-谷歌商店上架
继承超类 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 */@Slf4jpublic 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() 从存储系统中读取 OAuth2AccessTokenOAuth2AccessToken readAccessToken(String tokenValue);// 删除 OAuth2AccessToken// ☞ JwtTokenStore 并未实现void removeAccessToken(OAuth2AccessToken token);// 保存 RefreshToken. 参数是 OAuth2RefreshToken 和与之关联的 OAuth2Authentication// ☞ JwtTokenStore 并未实现void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);// 通过 OAuth2RefreshToken#getValue() 读取 OAuth2RefreshTokenOAuth2RefreshToken readRefreshToken(String tokenValue);// 从 OAuth2RefreshToken 中读取 OAuth2AuthenticationOAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);// 删除 OAuth2RefreshTokenvoid removeRefreshToken(OAuth2RefreshToken token);// 用 OAuth2RefreshToken 删除 OAuth2AccessToken (该功能能避免 RefreshToken 无限制的创建 AccessToken)// ☞ JwtTokenStore 并未实现void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);// 从 OAuth2Authentication 中获取 OAuth2AccessToken. 如果没有就返回 null// ☞ JwtTokenStore 并未实现, 始终返回 nullOAuth2AccessToken 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)).
自定义 SpringSecurity OAuth2 的 AccessToken 和 RefreshToken (采用 RSA 签名的 JWT)-第4张图片-谷歌商店上架

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

// 将 OAuth2AccessToken 和 OAuth2Authentication 转换成 Map<String, ?>Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);// 从 OAuth2AcccessToken#getValue() 和 信息 Map 中抽取 OAuth2AccessTokenOAuth2AccessToken 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 的记录)    @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 (证书链) 简述

使用以下命令生成一个JWT密钥对,并将其存储在名为authorization-server.jks的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/ptZpVuvCBQZ4dMeDhmZJmyxia7A9706B5o/ipLFcZnjOtKVQcZTa8UOniTDJ46DmMyK2Q5oW8d24cpMdPSwxNMU/7dOv40DFnoFUFIWUR/+fAZVTCfJb7pBpzWpmLmvOhLV8rSOKbJTIeRUWgsFZsCJJaqIa3/6k7moTV4DURUgh1ABmMyXUd3/zeSkdPJXu9QCdxFygSPVJs4d5Bqr97mROIdt9qmngap1Lch2elwrzWuQx63mGxoK+lxEQB6ftdPLvpEABuCBs7hO18CBj5ei9G+foaFe/77muNCILAtvc8UiD6PRbf5e1YXEp0IHZisuOhedjqBFQIDAQAB-----END PUBLIC KEY----------BEGIN CERTIFICATE-----MIIDbzCCAlegAwIBAgIEAfMOsjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHU2ljaHVhbjEQMA4GA1UEBxMHQ2hlbmdkdTEQMA4GA1UEChMHY2FwbGlrZTERMA8GA1UECxMIcGVyc29uYWwxEDAOBgNVBAMTB2NhcGxpa2UwHhcNMjAwNzE3MDc0MzU0WhcNMzAwNzE1MDc0MzU0WjBoMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHU2ljaHVhbjEQMA4GA1UEBxMHQ2hlbmdkdTEQMA4GA1UEChMHY2FwbGlrZTERMA8GA1UECxMIcGVyc29uYWwxEDAOBgNVBAMTB2NhcGxpa2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUvHlvPfO7+m1mlW68IFBnh0x4OGZkmbLGJrsD3vToHmj+KksVxmeM60pVBxlNrxQ6eJMMnjoOYzIrZDmhbx3bhykx09LDE0xT/t06/jQMWegVQUhZRH/58BlVMJ8lvukGnNamYua86EtXytI4pslMh5FRaCwVmwIklqohrf/qTuahNXgNRFSCHUAGYzJdR3f/N5KR08le71AJ3EXKBI9Umzh3kGqv3uZE4h232qaeBqnUtyHZ6XCvNa5DHreYbGgr6XERAHp+108u+kQAG4IGzuE7XwIGPl6L0b5+hoV7/vua40IgsC29zxSIPo9Ft/l7VhcSnQgdmKy46F52OoEVAgMBAAGjITAfMB0GA1UdDgQWBBRqowFVjNkW77ZciS10KyMWs/3n2jANBgkqhkiG9w0BAQsFAAOCAQEAJ+d+/0ss/Hl8IhPuIbH5Hh3MMxK8f02/QBPyJ5+ZJgt9k1BZc6/eMYbWd41z05gb2m2arXfAS2HEdsY1pCfcssb85cVYUwMoDfK7pLRX34V0uhdUm0wqTBumIs2iCCLCz7Eci4XpAv+RWHVKXbg+pP7GrKBh0iNYTuV+pDr+D7K6rZwGjYsGAqqpc1LjNNaN68pHhTnwXu4igM/gLsNRmR+2zXyJ1FZegnk0fsFWojOqHwCZxYli9245N4HgePIVTvFTu+QzdLzFUcsGqhrynHfwQOvTyPMpaowpOsguNSzTdmRRK3QdtKHglE10us40NUJZQgavCigGcVwAv/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@EnableAuthorizationServerpublic 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
} 
{"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@EnableResourceServerpublic 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@Componentpublic 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

标签: 服务器 资源 方法 接口

发表评论 (已有0条评论)

还木有评论哦,快来抢沙发吧~