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
refreshTokenValiditySecondsrefresh_token 的有效时长 (秒), 默认 30 天
accessTokenValiditySecondsaccess_token 的有效时长 (秒), 默认 12 小时
supportRefreshToken是否支持 refresh token, 默认为 false
reuseRefreshToken是否复用 refresh_token, 默认为 true (如果为 false, 每次请求刷新都会删除旧的 refresh_token, 创建新的 refresh_token)
tokenStoretoken 储存器 (持久化容器) (下篇文章会介绍)
clientDetailsService提供 client 详情的服务 (clientDetails 可持久化到数据库中或直接放在内存里)
accessTokenEnhancertoken 增强器, 可以通过实现 TokenEnhancer 以存放 additional information
authenticationManagerAuthentication 管理者, 起到填充完整 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);
    }

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

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值