Spring Security OAuth2 源码分析3 - 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);
    }

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

该系列文章:

Spring Security OAuth2 源码分析1 - TokenEndpoint
Spring Security OAuth2 源码分析2 - TokenGranter
Spring Security OAuth2 源码分析3 - TokenServices

持续更新中...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容