TokenGranter 获取 Token 的最后一步中, 调用了 tokenServices 的 createAccessToken 方法,源码如下:
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
为了进一步地了解 OAuth2AccessToken 的获取过程,本文将详细介绍
AuthorizationServerTokenServices 和 ResourceServerTokenServices。
接口定义了三个方法: createAccessToken (创建访问令牌)、refreshAccessToken (刷新访问令牌)、getAccessToken (获取访问令牌)。接口源码如下:
/**
* @author Ryan Heaton
* @author Dave Syer
*/
public interface AuthorizationServerTokenServices {
/**
* Create an access token associated with the specified credentials.
* @param authentication The credentials associated with the access token.
* @return The access token.
* @throws AuthenticationException If the credentials are inadequate.
*/
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
/**
* Refresh an access token. The authorization request should be used for 2 things (at least): to validate that the
* client id of the original access token is the same as the one requesting the refresh, and to narrow the scopes
* (if provided).
*
* @param refreshToken The details about the refresh token.
* @param tokenRequest The incoming token request.
* @return The (new) access token.
* @throws AuthenticationException If the refresh token is invalid or expired.
*/
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
throws AuthenticationException;
/**
* Retrieve an access token stored against the provided authentication key, if it exists.
*
* @param authentication the authentication key for the access token
*
* @return the access token or null if there was none
*/
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}
它的实现类有 DefaultTokenServices, 下文将详细展开介绍。
接口定义了两个方法: loadAuthentication (加载凭据)、readAccessToken (获取 access token 的详情)。接口源码如下:
public interface ResourceServerTokenServices {
/**
* Load the credentials for the specified access token.
*
* @param accessToken The access token value.
* @return The authentication for the access token.
* @throws AuthenticationException If the access token is expired
* @throws InvalidTokenException if the token isn't valid
*/
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
/**
* Retrieve the full access token details from just the value.
*
* @param accessToken the token value
* @return the full access token with client id etc.
*/
OAuth2AccessToken readAccessToken(String accessToken);
}
它的实现类有 RemoteTokenServices、DefaultTokenServices。
以下是 DefaultTokenServices 的关键属性:
属性 | note |
---|---|
refreshTokenValiditySeconds | refresh_token 的有效时长 (秒), 默认 30 天 |
accessTokenValiditySeconds | access_token 的有效时长 (秒), 默认 12 小时 |
supportRefreshToken | 是否支持 refresh token, 默认为 false |
reuseRefreshToken | 是否复用 refresh_token, 默认为 true (如果为 false, 每次请求刷新都会删除旧的 refresh_token, 创建新的 refresh_token) |
tokenStore | token 储存器 (持久化容器) (下篇文章会介绍) |
clientDetailsService | 提供 client 详情的服务 (clientDetails 可持久化到数据库中或直接放在内存里) |
accessTokenEnhancer | token 增强器, 可以通过实现 TokenEnhancer 以存放 additional information |
authenticationManager | Authentication 管理者, 起到填充完整 Authentication的作用 |
在认证服务的 Endpoints 中, 使用的正是 DefaultTokenServices, 它为 DefaultTokenServices 提供了默认配置, 源码如下:
public final class AuthorizationServerEndpointsConfigurer {
// 省略部分代码, 只看默认配置相关 ...
private DefaultTokenServices createDefaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
// 如果未配置, 则配置为 InMemoryClientDetailsService
tokenServices.setClientDetailsService(clientDetailsService());
tokenServices.setTokenEnhancer(tokenEnhancer());
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}
private TokenStore tokenStore() {
// 如果未配置, 则创建
if (tokenStore == null) {
// 如果配置了 JwtAccessTokenConverter, 则创建 JwtTokenStore
if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
}
// 否则, 创建 InMemoryTokenStore
else {
this.tokenStore = new InMemoryTokenStore();
}
}
return this.tokenStore;
}
private TokenEnhancer tokenEnhancer() {
// 如果未配置 tokenEnhancer, 但配置了 JwtAccessTokenConverter, 则将这个 convert 返回
if (this.tokenEnhancer == null && accessTokenConverter() instanceof JwtAccessTokenConverter) {
tokenEnhancer = (TokenEnhancer) accessTokenConverter;
}
return this.tokenEnhancer;
}
// ...
}
实际业务场景研发可以通过配置 AuthorizationServerEndpointsConfigurer 以自定义 token 的持久化策略、token 的刷新机制等等。(下一篇文章将会具体介绍 TokenStore, 使我们更好地了解 token 的自定义存储)。
如何创建 OAuth2AccessToken? 我们来读读它的源码:
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 从 tokenStore 中获取现存的 accessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 如果 existingAccessToken 存在
if (existingAccessToken != null) {
// 看是否过期
if (existingAccessToken.isExpired()) {
// 既然 existingAccessToken 已经过期了, 则将对应的 refresh_token 和自己删掉
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
// 如果没过期则重新存到 tokenStore
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// 如果 existingAccessToken 不存在或者存在但是已经过期了, 往下走
// 如果 existingAccessToken 不存在或过期了但它里边没有 refresh_token 信息, 则创建新的 refresh_token
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// 如果 existingAccessToken 过期了, 并且存在 refresh_token, 并且这个 refresh_token 也过期了, 则新创建一个 refresh_token
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 创建新的 accessToken 并存到 tokenStore 中
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// 将 accessToken 中的 refreshToken 也存到 tokenStore 中
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
一个新的 OAuth2AccessToken 和 OAuth2RefreshToken 是如何创建的? 它的源码如下:
private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
// 如果属性 supportRefreshToken 为 false, 则返回 null
if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
return null;
}
// token 的值其实是一个 UUID, 通过实例化 DefaultExpiringOAuth2RefreshToken 创建有时效性的 token
int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
String value = UUID.randomUUID().toString();
if (validitySeconds > 0) {
return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
+ (validitySeconds * 1000L)));
}
return new DefaultOAuth2RefreshToken(value);
}
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());
// 如果属性 accessTokenEnhancer 不为空, 则拓展 token 的信息 (原理是给这个 token setAdditionalInformation)
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
看完这个新建 token 的过程, 我们大概知道刚刚那些配置属性的去处了。
刚刚我们创建了 OAuth2AccessToken, 这时我们要怎么把它拿出来呢? 我们继续读源码:
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
return tokenStore.getAccessToken(authentication);
}
它是根据参数 OAuth2Authentication (身份验证令牌, 包含着用户信息, 下篇文章会着重介绍, 这里简单了解即可) 直接读取。
我们创建的 token 是有时效性的, 所以为了让它不过期得刷新。我们通过源码看看 spring security 是如何刷新 token 的:
@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
throws AuthenticationException {
// 如果 supportRefreshToken 为 false, 则直接抛出异常
if (!supportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
// 根据 value 获取 OAuth2RefreshToken
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
if (refreshToken == null) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
// 获取 OAuth2RefreshToken 中的 OAuth2Authentication
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) {
// The client has already been authenticated, but the user authentication might be old now, so give it a
// chance to re-authenticate.
Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
user = authenticationManager.authenticate(user);
Object details = authentication.getDetails();
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
authentication.setDetails(details);
}
// 从 OAuth2Authentication 中拿 clientId 看是否和 tokenRequest 中的一致, 如果不一致, 抛异常
String clientId = authentication.getOAuth2Request().getClientId();
if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
}
// 通过 refreshToken 删除 accessToken (它们之间通过共同的 refreshTokenValue 联系着)
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
// 如果 refresh_token 过期了, 则删掉并抛出异常
if (isExpired(refreshToken)) {
tokenStore.removeRefreshToken(refreshToken);
throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
}
// 如果 refreshToken 没过期, 则创建新的 OAuth2Authentication
authentication = createRefreshedAuthentication(authentication, tokenRequest);
// 如果设置了属性 reuseRefreshToken 为false, 则删除旧的 refreshToken, 然后根据新的 OAuth2Authentication 创建新的 refreshToken
if (!reuseRefreshToken) {
tokenStore.removeRefreshToken(refreshToken);
refreshToken = createRefreshToken(authentication);
}
// 创建新的 accessToken 并储存至 tokenStore (刷新)
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// 如果选择不复用, 则储存新的 refreshToken
if (!reuseRefreshToken) {
tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
}
return accessToken;
}
从 tokenStore 中直接读取 accessToken, 源码如下:
public OAuth2AccessToken readAccessToken(String accessToken) {
return tokenStore.readAccessToken(accessToken);
}
接下来我们看看它是如何加载凭证信息的?
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
InvalidTokenException {
// 根据 token value 从 tokenStore 中获取 OAuth2AccessToken
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
// 校验 accessToken, 如果为空则抛异常, 如果过期了则删除并抛出异常
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
// 如果 accessToken 没问题, 则从中读取 OAuth2Authentication
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
if (result == null) {
// in case of race condition
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
// 校验 OAuth2Authentication 无误则将之返回 (根据 result 中 clientId 是否能获取到 client 信息, 如果不能则抛出异常)
if (clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
}
catch (ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
return result;
}
远程令牌服务, 它通过配置的 checkTokenEndpointUrl 请求得到凭证信息。源码如下:
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap formData = new LinkedMultiValueMap();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
// gh-838
if (!Boolean.TRUE.equals(map.get("active"))) {
logger.debug("check_token returned active attribute: " + map.get("active"));
throw new InvalidTokenException(accessToken);
}
return tokenConverter.extractAuthentication(map);
}
我们可以通过这种设计方法灵活地获取用户凭证信息。