文章目录
AuthorizationServer
引言
本文在 前一篇 基础上构建.
- 全面的令牌自定义, 包含:
AuthorizationServerTokenSerivces
的自定义;TokenStore
的自定义,TokenEnhancer
自定义;OAuth2AccessToken
的自定义;- 启用 JWT (Json Web Token);
在前面的 DEMO 中, 我们已经自定义了 TokenGranter
:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// ...
/**
* Description: 配置 {@link AuthorizationServerEndpointsConfigurer}<br>
* Details: 配置授权服务器端点的非安全性特性, 例如 令牌存储, 自定义. 如果是密码授权, 需要在这里提供一个 {@link AuthenticationManager}
*
* @see AuthorizationServerConfigurerAdapter#configure(AuthorizationServerEndpointsConfigurer)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// @formatter:off
// 对于密码授权模式, 需要提供 AuthenticationManager 用于用户信息的认证
endpoints
.authenticationManager(authenticationManager)
// ~ 自定义的 WebResponseExceptionTranslator, 默认使用 DefaultWebResponseExceptionTranslator, 在 /oauth/token 端点
// ref: TokenEndpoint
.exceptionTranslator(webResponseExceptionTranslator)
// ~ 自定义的 TokenGranter
.tokenGranter(new CustomTokenGranter(endpoints, authenticationManager))
// .tokenServices(AuthorizationServerTokenServices)
// .tokenStore(TokenStore)
// .tokenEnhancer(TokenEnhancer)
// ~ refresh_token required
.userDetailsService(userDetailsService)
;
// @formatter:on
}
// ...
}
CustomTokenGranter
自身委托 CompositeTokenGranter
来颁发令牌.
AuthorizationServerTokenServices
AuthorizationServerTokenServices
为授权服务器提供 “创建”, “更新”, “获取” OAuth2AccessToken
的方法接口. 主要职责上是把认证信息 (Authentication
) "塞"到 AccessToken 中.
默认实现: DefaultTokenServices
(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultAuthorizationServerTokenServices
). 支持AuthorizationServerEndpointsConfigurer#tokenServices(AuthorizationServerTokenServices)
自定义配置.
来看看 AuthorizationServerTokenServices
接口的方法签名:
// 根据指定的凭证信息, 创建 AccessToken
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
// 刷新 AccessToken.
// 第二个参数: 认证请求 (TokenRequest) 被用来验证原来 AccessToken 中的客户端ID是否与刷新请求中的一致, 和用于缩小 Scope
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException;
// 从 OAuth2Authentication 中获取 OAuth2AccessToken
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
对于每一种 TokenGranter
的实现 (AuthorizationCodeTokenGranter
, RefreshTokenGranter
, ImplicitTokenGranter
, ClientCredentialsTokenGranter
, ResourceOwnerPasswordTokenGranter
), 在 OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest)
方法最后, 会调用 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
*/
@Slf4j
public class CustomTokenGranter implements TokenGranter {
/**
* 委托 {@link CompositeTokenGranter}
*/
private final CompositeTokenGranter delegate;
/**
* Description: 构建委托对象 {@link CompositeTokenGranter}
*
* @param configurer {@link AuthorizationServerEndpointsConfigurer}
* @param authenticationManager {@link AuthenticationManager}, grantType 为 password 时需要
* @author LiKe
* @date 2020-06-23 15:28:24
*/
public CustomTokenGranter(AuthorizationServerEndpointsConfigurer configurer, AuthenticationManager authenticationManager) {
final ClientDetailsService clientDetailsService = configurer.getClientDetailsService();
final AuthorizationServerTokenServices tokenServices = configurer.getTokenServices();
final AuthorizationCodeServices authorizationCodeServices = configurer.getAuthorizationCodeServices();
final OAuth2RequestFactory requestFactory = configurer.getOAuth2RequestFactory();
this.delegate = new CompositeTokenGranter(Arrays.asList(
new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory),
new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory),
new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory),
new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory),
new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory)
));
}
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
log.debug("Custom TokenGranter :: grant token with type {}", grantType);
// 如果发生异常, 会触发 WebResponseExceptionTranslator
final OAuth2AccessToken oAuth2AccessToken =
Optional.ofNullable(delegate.grant(grantType, tokenRequest)).orElseThrow(() -> new UnsupportedGrantTypeException("不支持的授权类型!"));
return new CustomOAuth2AccessToken(oAuth2AccessToken);
}
/**
* 自定义 {@link CustomOAuth2AccessToken}
*/
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = CustomOAuth2AccessTokenJackson2Serializer.class)
public static final class CustomOAuth2AccessToken extends DefaultOAuth2AccessToken {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public CustomOAuth2AccessToken(OAuth2AccessToken accessToken) {
super(accessToken);
}
/**
* Description: 序列化 {@link OAuth2AccessToken}
*
* @return 形如 { "access_token": "aa5a459e-4da6-41a6-bf67-6b8e50c7663b", "token_type": "bearer", "expires_in": 119, "scope": "read_scope" } 的字符串
* @see OAuth2AccessTokenJackson1Serializer
*/
@SneakyThrows
public String tokenSerialize() {
final LinkedHashMap<Object, Object> map = new LinkedHashMap<>(5);
map.put(OAuth2AccessToken.ACCESS_TOKEN, this.getValue());
map.put(OAuth2AccessToken.TOKEN_TYPE, this.getTokenType());
final OAuth2RefreshToken refreshToken = this.getRefreshToken();
if (Objects.nonNull(refreshToken)) {
map.put(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());
}
final Date expiration = this.getExpiration();
if (Objects.nonNull(expiration)) {
map.put(OAuth2AccessToken.EXPIRES_IN, (expiration.getTime() - System.currentTimeMillis()) / 1000);
}
final Set<String> scopes = this.getScope();
if (!CollectionUtils.isEmpty(scopes)) {
final StringBuffer buffer = new StringBuffer();
scopes.stream().filter(StringUtils::isNotBlank).forEach(scope -> buffer.append(scope).append(" "));
map.put(OAuth2AccessToken.SCOPE, buffer.substring(0, buffer.length() - 1));
}
final Map<String, Object> additionalInformation = this.getAdditionalInformation();
if (!CollectionUtils.isEmpty(additionalInformation)) {
additionalInformation.forEach((key, value) -> map.put(key, additionalInformation.get(key)));
}
return OBJECT_MAPPER.writeValueAsString(map);
}
}
/**
* 自定义 {@link CustomOAuth2AccessToken} 的序列化器
*/
private static final class CustomOAuth2AccessTokenJackson2Serializer extends StdSerializer<CustomOAuth2AccessToken> {
protected CustomOAuth2AccessTokenJackson2Serializer() {
super(CustomOAuth2AccessToken.class);
}
@Override
public void serialize(CustomOAuth2AccessToken oAuth2AccessToken, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField(SecurityResponse.FIELD_HTTP_STATUS, HttpStatus.OK.value());
jsonGenerator.writeObjectField(SecurityResponse.FIELD_TIMESTAMP, LocalDateTime.now().format(DateTimeFormatter.ofPattern(SecurityResponse.TIME_PATTERN, Locale.CHINA)));
jsonGenerator.writeObjectField(SecurityResponse.FIELD_MESSAGE, HttpStatus.OK.getReasonPhrase());
jsonGenerator.writeObjectField(SecurityResponse.FIELD_DATA, oAuth2AccessToken.tokenSerialize());
jsonGenerator.writeEndObject();
}
}
}
自定义 AuthorizationServerTokenServices
从上一章可以看到, 对于每个授权类型对应的具体的 Granter, 都构造依赖 tokenServices. 所有 “具体的” Granter 都继承于 AbstractTokenGranter
, 后者在构建 OAuth2AccessToken
之前会调用 tokenServices 的 createAccessToken. 本身, 对于令牌的 “业务性” 操作都是委托 tokenServices 来进行的. 而 “持久化” 操作, 则是委托 TokenStore
来完成.
代码实现 - CustomAuthorizationServerTokenServices
这是我们自定的 AuthorizationServerTokenServices
完整代码:
/**
* 自定义的 {@link AuthorizationServerTokenServices}
*
* @author LiKe
* @version 1.0.0
* @date 2020-07-08 14:59
* @see org.springframework.security.oauth2.provider.token.DefaultTokenServices
* @see AuthorizationServerTokenServices
*/
public class CustomAuthorizationServerTokenServices extends DefaultTokenServices {
// ~ Necessary
// -----------------------------------------------------------------------------------------------------------------
/**
* 自定义的持久化令牌的接口 {@link TokenStore} 引用
*/
private final TokenStore tokenStore;
/**
* 自定义的 {@link ClientDetailsService} 的引用
*/
private final ClientDetailsService clientDetailsService;
// ~ Optional
// -----------------------------------------------------------------------------------------------------------------
/**
* {@link AuthenticationManager}
*/
private AuthenticationManager authenticationManager;
// =================================================================================================================
/**
* {@link TokenEnhancer}
*/
private final TokenEnhancer accessTokenEnhancer;
private final TokenGenerator tokenGenerator = new TokenGenerator();
/**
* Description: 构建 {@link AuthorizationServerTokenServices}<br>
* Details: 依赖 {@link TokenStore}, {@link org.springframework.security.oauth2.provider.ClientDetailsService}\
*
* @param endpoints {@link AuthorizationServerEndpointsConfigurer}
* @author LiKe
* @date 2020-07-08 15:24:18
*/
public CustomAuthorizationServerTokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
this.tokenStore = Objects.requireNonNull(endpoints.getTokenStore(), "tokenStore 不能为空!");
this.clientDetailsService = Objects.requireNonNull(endpoints.getClientDetailsService(), "clientDetailsService 不能为空!");
final TokenEnhancer tokenEnhancer = Objects.requireNonNull(endpoints.getTokenEnhancer(), "tokenEnhancer 不能为空!");
Assert.assignable(JwtAccessTokenConverter.class, tokenEnhancer.getClass(), () -> new RuntimeException("tokenEnhancer 必须是 JwtAccessTokenConverter 的实例!"));
this.accessTokenEnhancer = tokenEnhancer;
}
/**
* 创建 access-token
*
* @see org.springframework.security.oauth2.provider.token.DefaultTokenServices#createAccessToken(OAuth2Authentication)
*/
@Override
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 当前客户端是否支持 refresh_token
final boolean supportRefreshToken = isSupportRefreshToken(authentication);
OAuth2RefreshToken existingRefreshToken = null;
// 如果已经存在令牌
final OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
if (Objects.nonNull(existingAccessToken)) {
if (existingAccessToken.isExpired()) {
// 如果已过期, 则删除 AccessToken 和 RefreshToken
if (supportRefreshToken) {
existingRefreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(existingRefreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
} else {
// 否则重新保存令牌 (以防 authentication 已经改变)
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// 生成新的 refresh_token
OAuth2RefreshToken newRefreshToken = null;
if (supportRefreshToken) {
if (Objects.isNull(existingRefreshToken)) {
// 如果没有 RefreshToken, 生成一个
newRefreshToken = tokenGenerator.createRefreshToken(authentication);
} else if (existingRefreshToken instanceof ExpiringOAuth2RefreshToken) {
// 如果有 RefreshToken 但是已经过期, 重新颁发
if (System.currentTimeMillis() > ((ExpiringOAuth2RefreshToken) existingRefreshToken).getExpiration().getTime()) {
newRefreshToken = tokenGenerator.createRefreshToken(authentication);
}
}
}
// 生成新的 access_token
final OAuth2AccessToken newAccessToken = tokenGenerator.createAccessToken(authentication, newRefreshToken);
if (supportRefreshToken) {
tokenStore.storeRefreshToken(newRefreshToken, authentication);
}
tokenStore.storeAccessToken(newAccessToken, authentication);
return newAccessToken;
}
/**
* 刷新 access-token
*
* @see org.springframework.security.oauth2.provider.token.DefaultTokenServices#refreshAccessToken(String, TokenRequest)
*/
@Override
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
final String clientId = tokenRequest.getClientId();
if (Objects.isNull(clientId) || !StringUtils.equals(clientId, tokenRequest.getClientId())) {
throw new InvalidGrantException(String.format("错误的客户端: %s, refresh token: %s", clientId, refreshTokenValue));
}
if (!isSupportRefreshToken(clientId)) {
throw new InvalidGrantException(String.format("客户端 (%s) 不支持 refresh_token!", clientId));
}
final OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
if (Objects.isNull(refreshToken)) {
throw new InvalidTokenException(String.format("无效的 refresh_token: %s!", refreshTokenValue));
}
// ~ 用 refresh_token 获取 OAuth2 认证信息
OAuth2Authentication oAuth2Authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (Objects.nonNull(this.authenticationManager) && !oAuth2Authentication.isClientOnly()) {
oAuth2Authentication = new OAuth2Authentication(
oAuth2Authentication.getOAuth2Request(),
authenticationManager.authenticate(
new PreAuthenticatedAuthenticationToken(oAuth2Authentication.getUserAuthentication(), StringUtils.EMPTY, oAuth2Authentication.getAuthorities())
)
);
oAuth2Authentication.setDetails(oAuth2Authentication.getDetails());
}
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
if (isExpired(refreshToken)) {
tokenStore.removeRefreshToken(refreshToken);
throw new InvalidTokenException("无效的 refresh_token (已过期)!");
}
// ~ 刷新 OAuth2 认证信息, 并基于此构建新的 OAuth2AccessToken
oAuth2Authentication = createRefreshedAuthentication(oAuth2Authentication, tokenRequest);
// 获取新的 refresh_token
final OAuth2AccessToken refreshedAccessToken = tokenGenerator.createAccessToken(oAuth2Authentication, refreshToken);
tokenStore.storeAccessToken(refreshedAccessToken, oAuth2Authentication);
return refreshedAccessToken;
}
@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
return tokenStore.getAccessToken(authentication);
}
// -----------------------------------------------------------------------------------------------------------------
/**
* Description: 判断当前客户端是否支持 refreshToken
*
* @param authentication {@link OAuth2Authentication}
* @return boolean
* @author LiKe
* @date 2020-07-08 18:16:09
*/
private boolean isSupportRefreshToken(OAuth2Authentication authentication) {
return isSupportRefreshToken(authentication.getOAuth2Request().getClientId());
}
/**
* Description: 判断当前客户端是否支持 refreshToken
*
* @param clientId 客户端 ID
* @return boolean
* @author LiKe
* @date 2020-07-09 10:02:11
*/
private boolean isSupportRefreshToken(String clientId) {
return clientDetailsService.loadClientByClientId(clientId).getAuthorizedGrantTypes().contains("refresh_token");
}
/**
* Create a refreshed authentication.<br>
* <i>(Copied from DefaultTokenServices#createRefreshedAuthentication(OAuth2Authentication, TokenRequest))</i>
*
* @param authentication The authentication.
* @param tokenRequest The scope for the refreshed token.
* @return The refreshed authentication.
* @throws InvalidScopeException If the scope requested is invalid or wider than the original scope.
*/
private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, TokenRequest tokenRequest) {
Set<String> tokenRequestScope = tokenRequest.getScope();
OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(tokenRequest);
if (Objects.nonNull(tokenRequestScope) && !tokenRequestScope.isEmpty()) {
Set<String> originalScope = clientAuth.getScope();
if (Objects.isNull(originalScope) || !originalScope.containsAll(tokenRequestScope)) {
throw new InvalidScopeException("Unable to narrow the scope of the client authentication to " + tokenRequestScope + ".", originalScope);
} else {
clientAuth = clientAuth.narrowScope(tokenRequestScope);
}
}
return new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());
}
// =================================================================================================================
/**
* Description: 令牌生成器
*
* @author LiKe
* @date 2020-07-08 18:36:41
*/
private final class TokenGenerator {
/**
* Description: 创建 refresh-token<br>
* Details: 如果采用 JwtTokenStore, OAuth2RefreshToken 最终会在 JWtAccessTokenConverter 中被包装成用私钥加密后的以 OAuth2AccessToken 作为 payload 的 JWT 格式
*
* @param authentication {@link OAuth2Authentication}
* @return org.springframework.security.oauth2.common.OAuth2RefreshToken
* @author LiKe
* @date 2020-07-09 15:52:28
*/
public OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
if (!isSupportRefreshToken(authentication)) {
return null;
}
final int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
final String tokenValue = UUID.randomUUID().toString();
if (validitySeconds > 0) {
return new DefaultExpiringOAuth2RefreshToken(tokenValue, new Date(System.currentTimeMillis() + validitySeconds * 1000L));
}
// 返回不过期的 refresh-token
return new DefaultOAuth2RefreshToken(tokenValue);
}
/**
* Description: 创建 access-token<br>
* Details: 如果采用 JwtTokenStore, OAuth2AccessToken 最终会在 JWtAccessTokenConverter 中被包装成用私钥加密后的以 OAuth2AccessToken 作为 payload 的 JWT 格式
*
* @param authentication {@link OAuth2Authentication}
* @param refreshToken {@link OAuth2RefreshToken}
* @return org.springframework.security.oauth2.common.OAuth2AccessToken
* @author LiKe
* @date 2020-07-09 15:51:29
*/
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
final String tokenValue = UUID.randomUUID().toString();
final CustomTokenGranter.CustomOAuth2AccessToken accessToken = new CustomTokenGranter.CustomOAuth2AccessToken(tokenValue);
final int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
accessToken.setExpiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L));
}
accessToken.setRefreshToken(refreshToken);
accessToken.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer.enhance(accessToken, authentication);
}
}
// =================================================================================================================
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}
自定义 TokenStore
TokenStore
是一个提供持久化 OAuth2 Token 的接口. 因为我们要采用 JWT 的形式作为 access-token, 所以重点关注它的实现类之一: org.springframework.security.oauth2.provider.token.store.JwtTokenStore
:
概述:
JwtTokenStore
是TokenStore
的其中之一实现. 默认实现是从令牌本身读取数据, 而不会进行持久化. 它本身需要JwtAccessTokenConverter
(extendsTokenEnhancer
) 来将常规令牌转换成 JWT 令牌, 并且如果JwtAccessTokenConverter
设置了 keyPair (加密算法必须是 RSA),JwtAccessTokenConverter
就会对 access-token 和 refresh-token 用私钥加密, 公钥解密 (org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter#enhance(OAuth2AccessToken, OAuth2Authentication)
).
先来看看 TokenStore
的接口定义:
// 从 OAuth2AccessToken 中读取 OAuth2Authentication. 如果不存在则返回 null.
OAuth2Authentication readAuthentication(OAuth2AccessToken token);
// 从 OAuth2AccessToken#getValue() 中读取 OAuth2Authentication. 如果不存在则返回 null.
OAuth2Authentication readAuthentication(String token);
// 保存 AccessToken. 参数是 OAuth2AccessToken 和与之关键的 OAuth2Authentication
// ☞ JwtTokenStore 并未实现
void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
// 通过 OAuth2AccessToken#getValue() 从存储系统中读取 OAuth2AccessToken
OAuth2AccessToken readAccessToken(String tokenValue);
// 删除 OAuth2AccessToken
// ☞ JwtTokenStore 并未实现
void removeAccessToken(OAuth2AccessToken token);
// 保存 RefreshToken. 参数是 OAuth2RefreshToken 和与之关联的 OAuth2Authentication
// ☞ JwtTokenStore 并未实现
void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);
// 通过 OAuth2RefreshToken#getValue() 读取 OAuth2RefreshToken
OAuth2RefreshToken readRefreshToken(String tokenValue);
// 从 OAuth2RefreshToken 中读取 OAuth2Authentication
OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);
// 删除 OAuth2RefreshToken
void removeRefreshToken(OAuth2RefreshToken token);
// 用 OAuth2RefreshToken 删除 OAuth2AccessToken (该功能能避免 RefreshToken 无限制的创建 AccessToken)
// ☞ JwtTokenStore 并未实现
void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);
// 从 OAuth2Authentication 中获取 OAuth2AccessToken. 如果没有就返回 null
// ☞ JwtTokenStore 并未实现, 始终返回 null
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
// 通过 客户端ID 和 用户名 查询到与之关联的 OAuth2AccessToken.
// ☞ JwtTokenStore 并未实现, 始终返回空集合
Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);
// 通过 客户端ID 查询到与之关联的 OAuth2AccessToken.
// ☞ JwtTokenStore 并未实现, 始终返回空集合
Collection<OAuth2AccessToken> findTokensByClientId(String clientId);
JwtTokenStore
显示依赖一个名为 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 中抽取 OAuth2AccessToken
OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map);
// 通过从 AccessToken 中解码的信息 Map 抽取代表着 客户端 和 用户 (如果有) 的认证对象
OAuth2Authentication extractAuthentication(Map<String, ?> map);
JwtTokenStore
中有好几处都依赖了 JwtAccessTokenConverter
:
public class JwtTokenStore implements TokenStore {
// ...
// 将 JWT 格式的令牌的载荷转换成 Map 并从中抽取成认证对象 OAuth2Authentication
@Override
public OAuth2Authentication readAuthentication(String token) {
return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token));
}
// 1. 从 OAuth2AccessToken#getValue() 中抽取认证对象, 并联合 OAuth2AccessToken#getValue() 组装成 OAuth2AccessToken
// 2. 判断当前 OAuth2AccessToken 是否是一个 RefreshToken (根据其信息 Map 中是否包含键为 ati 的记录)
@Override
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = convertAccessToken(tokenValue);
if (jwtTokenEnhancer.isRefreshToken(accessToken)) {
throw new InvalidTokenException("Encoded token is a refresh token");
}
return accessToken;
}
private OAuth2AccessToken convertAccessToken(String tokenValue) {
return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
}
// 在从 tokenValue 中读取 OAuth2RefreshToken 的方法 OAuth2RefreshToken readRefreshToken(String tokenValue) 中, 同样调用了 JwtTokenEnhancer#isRefreshToken(OAuth2AccessToken) 用于判断是否是 RefreshToken.
private OAuth2RefreshToken createRefreshToken(OAuth2AccessToken encodedRefreshToken) {
if (!jwtTokenEnhancer.isRefreshToken(encodedRefreshToken)) {
throw new InvalidTokenException("Encoded token is not a refresh token");
}
if (encodedRefreshToken.getExpiration()!=null) {
return new DefaultExpiringOAuth2RefreshToken(encodedRefreshToken.getValue(),
encodedRefreshToken.getExpiration());
}
return new DefaultOAuth2RefreshToken(encodedRefreshToken.getValue());
}
// ...
}
签名与校验
对于 JWT 来说, 我们知道它本身是 Header, Payload 和 Signature 三部分以 . 拼接而成的字符串. 其中 Signature 是对前两部分的签名, 用于防止数据被篡改.
Reference: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
JwtAccessTokenConverter
的源代码中有几个比较关键的成员变量, 它们分别是:
private String verifierKey = new RandomValueStringGenerator().generate();
private Signer signer = new MacSigner(verifierKey);
private String signingKey = verifierKey;
private SignatureVerifier verifier;
先说说 verifierKey 和 verifier, 默认值是一个 6 位的随机字符串 (new RandomValueStringGenerator().generate()
), 和它 “配套” 的 verifier 是持有这个 verifierKey 的 MacSigner
(用 HMACSHA256 算法验证 Signature) / RsaVerifier
(用 RSA 公钥验证 Signature), 在 JwtAccessTokenConverter
的 decode 方法中作为签名校验器被传入 JwtHelper
的 decodeAndVerify(@NotNull String token, org.springframework.security.jwt.crypto.sign.SignatureVerifier verifier):
protected Map<String, Object> decode(String token) {
try {
Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
String content = jwt.getClaims();
Map<String, Object> map = objectMapper.parseMap(content);
if (map.containsKey(EXP) && map.get(EXP) instanceof Integer) {
Integer intValue = (Integer) map.get(EXP);
map.put(EXP, new Long(intValue));
}
return map;
}
catch (Exception e) {
throw new InvalidTokenException("Cannot convert access token to JSON", e);
}
}
而在 JwtHelper
的目标方法中, 首先把 token 的三个部分 (以 . 分隔的) 拆分出来, Base64.urlDecode
解码. 再用我们传入的 verifier 将 “Header.Payload” 编码 (如果是 RSA, 就是公钥.) 并与拆分出来的 Signature 部分比对 (Reference: org.springframework.security.jwt.crypto.sign.RsaVerifier#verify
).
对应的, signer 和 signingKey 作为签名 “组件” 存在, (可以看到在默认情况下, JwtAccessTokenConverter
对 JWT 的 Signature 采用的是对称加密, signingKey 和 verifierKey 一致) 在 JwtHelper
的 encode(@NotNull CharSequence content, @NotNull org.springframework.security.jwt.crypto.sign.Signer signer) 方法中, 被用于将 “Header.Payload” 加密 (如果是 RSA, 就是私钥) (Reference: org.springframework.security.jwt.crypto.sign.RsaSigner#sign
).
所以算法本质上不是对 JWT 整体进行加解密, 而是对其中的 Signature 部分
当然, 用户也可以通过 JwtAccessTokenConverter
提供的 setKeyPair(KeyPair) 自定义 RSA 的密钥对. 可以显示传入公私钥对, signer 持有私钥, verifier 持有公钥.
public void setKeyPair(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
signer = new RsaSigner((RSAPrivateKey) privateKey);
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
verifier = new RsaVerifier(publicKey);
verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
+ "\n-----END PUBLIC KEY-----";
}
(这个方法也是我们要用到的)
۞ 大致总结一下:
无论是默认的
DefaultTokenServices
还是我们自定义的AuthorizationServerTokenServices
, 在 createAccessToken 末尾都显示调用了 tokenEnhancer 来自定义令牌.
JwtTokenStore
(impements TokenStore) 提供了操作 JWT 形式令牌的接口, 具体实现里, 它借助JwtAccessTokenConverter
将包装和抽取令牌.
JwtAccessTokenConverter
本身实现了TokenEnhancer
和AccessTokenConverter
两个接口, 分别提供了包装令牌的方法实现, 和抽取令牌的方法实现.
代码实现 - JwtTokenStore
我们首先需要生成密钥对 (KeyPair) 和 KeyStore, 这里我们采用 PKCS#12 类型的密钥库, 与 JKS 类型的区别以及相关说明, 请查阅:
keytool -genkeypair -alias authorization-server-jwt-keypair -keyalg RSA -keysize 2048 -dname "CN=caplike, OU=personal, O=caplike, L=Chengdu, ST=Sichuan, C=CN" -validity 3650 -storetype JKS -keystore authorization-server.jks -storepass ********
执行如下命令从密钥库中导出公钥和证书的 PEM 格式:
keytool -list -rfc --keystore authorization-server.jks | openssl x509 -inform pem -pubkey
输入密钥库口令: ********
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlLx5bz3zu/ptZpVuvCBQ
Z4dMeDhmZJmyxia7A9706B5o/ipLFcZnjOtKVQcZTa8UOniTDJ46DmMyK2Q5oW8d
24cpMdPSwxNMU/7dOv40DFnoFUFIWUR/+fAZVTCfJb7pBpzWpmLmvOhLV8rSOKbJ
TIeRUWgsFZsCJJaqIa3/6k7moTV4DURUgh1ABmMyXUd3/zeSkdPJXu9QCdxFygSP
VJs4d5Bqr97mROIdt9qmngap1Lch2elwrzWuQx63mGxoK+lxEQB6ftdPLvpEABuC
Bs7hO18CBj5ei9G+foaFe/77muNCILAtvc8UiD6PRbf5e1YXEp0IHZisuOhedjqB
FQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDbzCCAlegAwIBAgIEAfMOsjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJD
TjEQMA4GA1UECBMHU2ljaHVhbjEQMA4GA1UEBxMHQ2hlbmdkdTEQMA4GA1UEChMH
Y2FwbGlrZTERMA8GA1UECxMIcGVyc29uYWwxEDAOBgNVBAMTB2NhcGxpa2UwHhcN
MjAwNzE3MDc0MzU0WhcNMzAwNzE1MDc0MzU0WjBoMQswCQYDVQQGEwJDTjEQMA4G
A1UECBMHU2ljaHVhbjEQMA4GA1UEBxMHQ2hlbmdkdTEQMA4GA1UEChMHY2FwbGlr
ZTERMA8GA1UECxMIcGVyc29uYWwxEDAOBgNVBAMTB2NhcGxpa2UwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUvHlvPfO7+m1mlW68IFBnh0x4OGZkmbLG
JrsD3vToHmj+KksVxmeM60pVBxlNrxQ6eJMMnjoOYzIrZDmhbx3bhykx09LDE0xT
/t06/jQMWegVQUhZRH/58BlVMJ8lvukGnNamYua86EtXytI4pslMh5FRaCwVmwIk
lqohrf/qTuahNXgNRFSCHUAGYzJdR3f/N5KR08le71AJ3EXKBI9Umzh3kGqv3uZE
4h232qaeBqnUtyHZ6XCvNa5DHreYbGgr6XERAHp+108u+kQAG4IGzuE7XwIGPl6L
0b5+hoV7/vua40IgsC29zxSIPo9Ft/l7VhcSnQgdmKy46F52OoEVAgMBAAGjITAf
MB0GA1UdDgQWBBRqowFVjNkW77ZciS10KyMWs/3n2jANBgkqhkiG9w0BAQsFAAOC
AQEAJ+d+/0ss/Hl8IhPuIbH5Hh3MMxK8f02/QBPyJ5+ZJgt9k1BZc6/eMYbWd41z
05gb2m2arXfAS2HEdsY1pCfcssb85cVYUwMoDfK7pLRX34V0uhdUm0wqTBumIs2i
CCLCz7Eci4XpAv+RWHVKXbg+pP7GrKBh0iNYTuV+pDr+D7K6rZwGjYsGAqqpc1Lj
NNaN68pHhTnwXu4igM/gLsNRmR+2zXyJ1FZegnk0fsFWojOqHwCZxYli9245N4Hg
ePIVTvFTu+QzdLzFUcsGqhrynHfwQOvTyPMpaowpOsguNSzTdmRRK3QdtKHglE10
us40NUJZQgavCigGcVwAv/jCdA==
-----END CERTIFICATE-----
或是直接导出证书:
keytool -exportcert -alias authorization-server-jwt-keypair -storetype PKCS12 -keystore authorization-server.jks -file public.cert -storepass ************
存储在文件 <public.cert> 中的证书.Reference: 从证书中读取公钥
分析就到这里, 下面我们为 AuthorizationServerEndpointsConfigurer
指定 tokenStore 和 tokenEnhancer:
/**
* 授权服务器配置类<br>
* {@code @EnableAuthorizationServer} 会启用 {@link org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint}
* 和 {@link org.springframework.security.oauth2.provider.endpoint.TokenEndpoint} 端点.
*
* @author LiKe
* @version 1.0.0
* @date 2020-06-15 09:43
* @see AuthorizationServerConfigurerAdapter
*/
@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
//...
/**
* Description: 配置 {@link AuthorizationServerEndpointsConfigurer}<br>
* Details: 配置授权服务器端点的非安全性特性, 例如 令牌存储, 自定义. 如果是密码授权, 需要在这里提供一个 {@link AuthenticationManager}
*
* @see AuthorizationServerConfigurerAdapter#configure(AuthorizationServerEndpointsConfigurer)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// @formatter:off
// 对于密码授权模式, 需要提供 AuthenticationManager 用于用户信息的认证
endpoints
.authenticationManager(authenticationManager)
// ~ 自定义的 WebResponseExceptionTranslator, 默认使用 DefaultWebResponseExceptionTranslator, 在 /oauth/token 端点
// ref: TokenEndpoint
.exceptionTranslator(webResponseExceptionTranslator)
// ~ 自定义的 TokenGranter
.tokenGranter(new CustomTokenGranter(endpoints, authenticationManager))
// ~ 自定义的 TokenStore
.tokenStore(tokenStore())
.tokenEnhancer(jwtAccessTokenConverter())
// ~ 自定义的 AuthorizationServerTokenServices
.tokenServices(new CustomAuthorizationServerTokenServices(endpoints))
// ~ refresh_token required
.userDetailsService(userDetailsService)
;
// @formatter:on
}
/**
* Description: 自定义 {@link JwtTokenStore}
*
* @return org.springframework.security.oauth2.provider.token.TokenStore {@link JwtTokenStore}
* @author LiKe
* @date 2020-07-20 18:11:25
*/
private TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* Description: 为 {@link JwtTokenStore} 所须
*
* @return org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter
* @author LiKe
* @date 2020-07-20 18:04:48
*/
private JwtAccessTokenConverter jwtAccessTokenConverter() {
final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("authorization-server.jks"), "********".toCharArray());
final JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("authorization-server-jwt-keypair"));
return jwtAccessTokenConverter;
}
//...
}
真正代码层面就这么点改动, 好了, 接下来我们启动服务器, 以密码授权形式请求授权服务器, 得到响应:
{
"status": 200,
"timestamp": "2020-07-21 15:51:58",
"message": "OK",
"data": "{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sImV4cCI6MTU5NTM2MTExOCwidXNlcl9uYW1lIjoiY2FwbGlrZSIsImp0aSI6ImRkZmExMTgwLTE0MDAtNDA0MC1iNjU3LTAzMTJmMWQ1OGIwNyIsImNsaWVudF9pZCI6ImNsaWVudC1hIiwic2NvcGUiOlsiQUNDRVNTX1JFU09VUkNFIl19.XHTbHaZnpudapYmKxx2RDwiaV71h0GvG61Dtgbc5VYTPN3xBoA1n6Ws8uSHd0tUFM-dpbqDOzL4RUNrXs-baTwVpTvBxtjNUdRh0fp3Vc3aMnWxkyQVivDVU_ZbDTSoqUrsJOBanNYH-V89jWP1H-V5bNUQK2EWWnz6xVWRHIcAMUJhW8ZC-rekcVk-v5wA4CJH9XFvkNbOsGOLIUYNVXGY27LhlGKWuXf1_EX-6kTMp7fKFwBlrjuujBn2NpRvzKxTyfW5O8czG-7hPDCumpfOlrTYlCOzTXc5Xr7hNUMZYfIurV6WtU5A__-nvQYRt3HLO48OXlsgAWn7e8NfrCg\",\"token_type\":\"bearer\",\"refresh_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sInVzZXJfbmFtZSI6ImNhcGxpa2UiLCJzY29wZSI6WyJBQ0NFU1NfUkVTT1VSQ0UiXSwiYXRpIjoiZGRmYTExODAtMTQwMC00MDQwLWI2NTctMDMxMmYxZDU4YjA3IiwiZXhwIjoxNTk3OTA5OTE4LCJqdGkiOiJiMTFjOGZkZi1lYzI4LTRmNWEtYjY0Ni1hZWVmNTJlNTQ4NDEiLCJjbGllbnRfaWQiOiJjbGllbnQtYSJ9.C-PMeXPLSDxBTpZE3m3dplAXF0BTV3OSOcRuOTTnZEvXStOLOfk7_SgTLetkzaZkoOO9pon7ezgceiFNOekHPM3SbNIgLpUKaXA3jrU3lYvuYqfqDjKHsL08wlzeCqdZL2vYpo_b7aRkKqEcar8_qEwEZBG9jVZVkZSLtAmwxW4HruPNe04EmbZiJsBT1NCGdAvWBbiHJ18ltZZROZWDILc7If9RCVp3U9AY5xAzE4BqIsZQ3zFiOv5RldfkJHYLmvlA0IjYbUSoSoeLqym_5YOWaAvTz1u0izAkXSScRwe5vfwJjwMr_0pXX6eACz1E4vPFRGdeOy_0iyyk17zT0Q\",\"expires_in\":43199,\"scope\":\"ACCESS_RESOURCE\",\"jti\":\"ddfa1180-1400-4040-b657-0312f1d58b07\"}"
}
(为什么响应是这种结构? 本文的代码是以 上一篇 为基础构建, 已经具备了统一响应格式的特性). 其中, data 为 CustomTokenGranter.CustomOAuth2AccessToken
序列化的结果:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sImV4cCI6MTU5NTM2MTExOCwidXNlcl9uYW1lIjoiY2FwbGlrZSIsImp0aSI6ImRkZmExMTgwLTE0MDAtNDA0MC1iNjU3LTAzMTJmMWQ1OGIwNyIsImNsaWVudF9pZCI6ImNsaWVudC1hIiwic2NvcGUiOlsiQUNDRVNTX1JFU09VUkNFIl19.XHTbHaZnpudapYmKxx2RDwiaV71h0GvG61Dtgbc5VYTPN3xBoA1n6Ws8uSHd0tUFM-dpbqDOzL4RUNrXs-baTwVpTvBxtjNUdRh0fp3Vc3aMnWxkyQVivDVU_ZbDTSoqUrsJOBanNYH-V89jWP1H-V5bNUQK2EWWnz6xVWRHIcAMUJhW8ZC-rekcVk-v5wA4CJH9XFvkNbOsGOLIUYNVXGY27LhlGKWuXf1_EX-6kTMp7fKFwBlrjuujBn2NpRvzKxTyfW5O8czG-7hPDCumpfOlrTYlCOzTXc5Xr7hNUMZYfIurV6WtU5A__-nvQYRt3HLO48OXlsgAWn7e8NfrCg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2Utc2VydmVyIl0sInVzZXJfbmFtZSI6ImNhcGxpa2UiLCJzY29wZSI6WyJBQ0NFU1NfUkVTT1VSQ0UiXSwiYXRpIjoiZGRmYTExODAtMTQwMC00MDQwLWI2NTctMDMxMmYxZDU4YjA3IiwiZXhwIjoxNTk3OTA5OTE4LCJqdGkiOiJiMTFjOGZkZi1lYzI4LTRmNWEtYjY0Ni1hZWVmNTJlNTQ4NDEiLCJjbGllbnRfaWQiOiJjbGllbnQtYSJ9.C-PMeXPLSDxBTpZE3m3dplAXF0BTV3OSOcRuOTTnZEvXStOLOfk7_SgTLetkzaZkoOO9pon7ezgceiFNOekHPM3SbNIgLpUKaXA3jrU3lYvuYqfqDjKHsL08wlzeCqdZL2vYpo_b7aRkKqEcar8_qEwEZBG9jVZVkZSLtAmwxW4HruPNe04EmbZiJsBT1NCGdAvWBbiHJ18ltZZROZWDILc7If9RCVp3U9AY5xAzE4BqIsZQ3zFiOv5RldfkJHYLmvlA0IjYbUSoSoeLqym_5YOWaAvTz1u0izAkXSScRwe5vfwJjwMr_0pXX6eACz1E4vPFRGdeOy_0iyyk17zT0Q",
"expires_in": 43199,
"scope": "ACCESS_RESOURCE",
"jti": "ddfa1180-1400-4040-b657-0312f1d58b07"
}
对于 access_token, 我们分别把它的 Header, Payload, Signature 解码得到:
{"alg":"RS256","typ":"JWT"}.{"aud":["resource-server"],"exp":1595361118,"user_name":"caplike","jti":"ddfa1180-1400-4040-b657-0312f1d58b07","client_id":"client-a","scope":["ACCESS_RESOURCE"]}.XHTbHaZnpudapYmKxx2RDwiaV71h0GvG61Dtgbc5VYTPN3xBoA1n6Ws8uSHd0tUFM-dpbqDOzL4RUNrXs-baTwVpTvBxtjNUdRh0fp3Vc3aMnWxkyQVivDVU_ZbDTSoqUrsJOBanNYH-V89jWP1H-V5bNUQK2EWWnz6xVWRHIcAMUJhW8ZC-rekcVk-v5wA4CJH9XFvkNbOsGOLIUYNVXGY27LhlGKWuXf1_EX-6kTMp7fKFwBlrjuujBn2NpRvzKxTyfW5O8czG-7hPDCumpfOlrTYlCOzTXc5Xr7hNUMZYfIurV6WtU5A__-nvQYRt3HLO48OXlsgAWn7e8NfrCg
而 refresh_token:
{"alg":"RS256","typ":"JWT"}.{"aud":["resource-server"],"user_name":"caplike","scope":["ACCESS_RESOURCE"],"ati":"ddfa1180-1400-4040-b657-0312f1d58b07","exp":1597909918,"jti":"b11c8fdf-ec28-4f5a-b646-aeef52e54841","client_id":"client-a"}.C-PMeXPLSDxBTpZE3m3dplAXF0BTV3OSOcRuOTTnZEvXStOLOfk7_SgTLetkzaZkoOO9pon7ezgceiFNOekHPM3SbNIgLpUKaXA3jrU3lYvuYqfqDjKHsL08wlzeCqdZL2vYpo_b7aRkKqEcar8_qEwEZBG9jVZVkZSLtAmwxW4HruPNe04EmbZiJsBT1NCGdAvWBbiHJ18ltZZROZWDILc7If9RCVp3U9AY5xAzE4BqIsZQ3zFiOv5RldfkJHYLmvlA0IjYbUSoSoeLqym_5YOWaAvTz1u0izAkXSScRwe5vfwJjwMr_0pXX6eACz1E4vPFRGdeOy_0iyyk17zT0Q
至此, 我们的自定义令牌应该算是初具规模了. 接下来, 还有几个细节需要 “打磨”, 请继续往下看…
ResourceServer
接下来我们编写资源服务器: 让资源服务器请求远端授权服务器的 CheckTokenEndpoint
端点, 验证签名并解析 JWT.
引言
ResourceServerTokenServices
ResourceServerTokenServices
默认有两个实现, 一个是 RemoteTokenServices
, 另外一个是 DefaultTokenServices
.
- 在资源服务器, 我们使用的
RemoteTokenServices
来像授权服务器发起检查并解析令牌的请求, 并用其结果封装成资源服务器的OAuth2Authentication
;
接下来我们调整资源服务器, 主要涉及的方面有:
- 通过
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
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource-server";
private static final String AUTHORIZATION_SERVER_CHECK_TOKEN_ENDPOINT_URL = "http://localhost:18957/token-customize-authorization-server/oauth/check_token";
// =================================================================================================================
private AuthenticationEntryPoint authenticationEntryPoint;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// @formatter:off
resources.resourceId(RESOURCE_ID).tokenServices(remoteTokenServices()).stateless(true);
resources.authenticationEntryPoint(authenticationEntryPoint);
// @formatter:on
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
// -----------------------------------------------------------------------------------------------------------------
/**
* Description: 远端令牌服务类<br>
* Details: 调用授权服务器的 /oauth/check_token 端点解析令牌. <br>
* 在本 DEMO 中, 调用授权服务器的 {@link org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint} 端点, <br>
* 将私钥签名的 JWT 发到授权服务器, 后者用公钥验证 Signature 部分
*
* @return org.springframework.security.oauth2.provider.token.RemoteTokenServices
* @author LiKe
* @date 2020-07-22 20:33:13
*/
private RemoteTokenServices remoteTokenServices() {
final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// ~ 设置 RestTemplate, 以自行决定异常处理
final RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
final int rawStatusCode = response.getRawStatusCode();
System.out.println(rawStatusCode);
if (rawStatusCode != 400) {
final String responseData = new String(super.getResponseBody(response));
throw new OAuth2Exception(responseData);
}
}
});
remoteTokenServices.setRestTemplate(restTemplate);
// ~ clientId 和 clientSecret 会以 base64(clientId:clientSecret) basic 方式请求授权服务器
remoteTokenServices.setClientId(RESOURCE_ID);
remoteTokenServices.setClientSecret("resource-server-p");
// ~ 请求授权服务器的 CheckTokenEndpoint 端点解析 JWT (AuthorizationServerEndpointsConfigurer 中指定的 tokenServices.
// 实现了 ResourceServerTokenServices 接口,
// 如果没有, 则使用默认的 (org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration.checkTokenEndpoint)
remoteTokenServices.setCheckTokenEndpointUrl(AUTHORIZATION_SERVER_CHECK_TOKEN_ENDPOINT_URL);
return remoteTokenServices;
}
// -----------------------------------------------------------------------------------------------------------------
@Autowired
public void setAuthenticationEntryPoint(@Qualifier("customAuthenticationEntryPoint") AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
}
为了达到这个目的, 当然的我们在资源服务器端也需要自定义 AuthenticationEntryPoint
:
(由于授权服务器返回的格式已经是 SecurityResponse
序列化的 (我们期望的) 标准结构. 所以这里, 我们只需要读取其内容即可. 譬如授权服务器返回的响应码, 也正是资源服务器要返向前端的响应码)
/**
* 自定义的 {@link AuthenticationEntryPoint}
*
* @author LiKe
* @version 1.0.0
* @date 2020-07-23 15:29
*/
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.debug("Custom AuthenticationEntryPoint triggered with exception: {}.", authException.getClass().getCanonicalName());
// 原始异常信息
final String authExceptionMessage = authException.getMessage();
try {
final SecurityResponse securityResponse = JSON.parseObject(authExceptionMessage, SecurityResponse.class);
ResponseWrapper.wrapResponse(response, securityResponse);
} catch (JSONException ignored) {
ResponseWrapper.forbiddenResponse(response, authExceptionMessage);
}
}
}
启动授权服务器和资源服务器, 当资源服务器以过期的令牌请求授权服务器时, 可以看到返回的正式我们期望的响应格式:
{
"timestamp": "2020-07-23 17:28:18",
"status": 403,
"message": "Cannot convert access token to JSON",
"data": "{}"
}
后记
但是这种在资源服务器通过使用 RemoteTokenServices
与授权服务器频繁交互的弊端也很明显, 每个携带令牌的请求都会与授权服务器交互一次: 授权服务器的压力过大, 设想我们有 N 个后端服务, 这带来的性能问题是不可忽视的. 下一篇, 我们将讨论如何 “解耦”, 让资源服务器 “自治”.
P.S.
本文是在 上一篇 的基础上做的扩展, 重复的部分没有赘述.
☞ 代码清参考: token-customize-resource-server-remote-token-services & token-customize-authorization-server