Spring Security OAuth2 源码分析(三) TokenServices

TokenGranter 获取 Token 的最后一步中, 调用了 tokenServices 的 createAccessToken 方法,源码如下:

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}

为了进一步地了解 OAuth2AccessToken 的获取过程,本文将详细介绍
AuthorizationServerTokenServices 和 ResourceServerTokenServices。

1. AuthorizationServerTokenServices 接口

接口定义了三个方法: 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, 下文将详细展开介绍。

2. ResourceServerTokenServices 接口

接口定义了两个方法: 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。

3. DefaultTokenServices

3.1 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 的自定义存储)。

3.2 DefaultTokenServices - createAccessToken

如何创建 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 的过程, 我们大概知道刚刚那些配置属性的去处了。

3.3 DefaultTokenServices - getAccessToken

刚刚我们创建了 OAuth2AccessToken, 这时我们要怎么把它拿出来呢? 我们继续读源码:

    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        return tokenStore.getAccessToken(authentication);
    }

它是根据参数 OAuth2Authentication (身份验证令牌, 包含着用户信息, 下篇文章会着重介绍, 这里简单了解即可) 直接读取。

3.4 DefaultTokenServices - refreshAccessToken

我们创建的 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;
    }

3.5 DefaultTokenServices - readAccessToken

从 tokenStore 中直接读取 accessToken, 源码如下:

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

3.6 DefaultTokenServices - loadAuthentication

接下来我们看看它是如何加载凭证信息的?

    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;
    }

4. RemoteTokenServices

远程令牌服务, 它通过配置的 checkTokenEndpointUrl 请求得到凭证信息。源码如下:

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

        MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
        formData.add(tokenName, accessToken);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
        Map<String, Object> 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);
    }

我们可以通过这种设计方法灵活地获取用户凭证信息。

猜你喜欢

转载自blog.csdn.net/u014252478/article/details/88693555