文章目录
- 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)
方法最后, 会调用 TokenServices
的 OAuth2AccessToken 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
也实现了AuthorizationServerTokenServices
和ResourceServerTokenServices
两个接口, 前者用于接收朝向授权服务器的令牌申请, 刷新请求; 而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 */@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
:
概述:
JwtTokenStore
是 TokenStore
的其中之一实现. 默认实现是从令牌本身读取数据, 而不会进行持久化. 它本身需要 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
显示依赖一个名为 JwtAccessTokenConverter
的 TokenEnhancer
:
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 中抽取 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
本身实现了TokenEnhancer
和AccessTokenConverter
两个接口, 分别提供了包装令牌的方法实现, 和抽取令牌的方法实现.
代码实现 - 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
;
接下来我们调整资源服务器, 主要涉及的方面有:
- 通过
RemoteTokenServices
请求授权服务器解析令牌. - 资源服务器响应格式一致性.
调整 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
还木有评论哦,快来抢沙发吧~