Spring OAuth2 实现始终获取新的令牌

Spring基于OAuth2协议编写的spring-oauth2实现,是行业级的接口资源安全解决方案,我们可以基于该依赖配置不同客户端的不同权限来访问接口数据。

推荐阅读

默认令牌生成方式

每当我们获取请求令牌(access_token)时,默认情况返回第一次生成的令牌,使用同一个用户多次获取令牌时,只有过期时间在缩短,其它的内容不变。

这种方式有利有弊,如果同一个账户只能有一个人登录,这样是没有任何问题的,但是如果同一个账号可以让多个人同时登录,那么就会存在一定的问题。

比如我们现在有一个名为hengboy的账户:第一个人登录时令牌有效期为我们配置的最长有效期(假设为7200秒),这时又有第二个人登录的同一个用户,第二个人获取的令牌并不会重置有效期(可能还剩下3000秒),对于这种结果并不是我们期望的

原因分析

目前spring-oauth2依赖内集成了三种存储令牌的方式,分别是:InMemoryTokenStore(内存方式)RedisTokenStore(Redis方式)JdbcTokenStore(数据库方式)

从阅读源码中可以发现无论我们配置使用什么方式来进行存储令牌,同一个账户的有效令牌只会存在一个,结合上面的场景来思考所以第二个人获取的令牌与第一个人是同一个。

DefaultTokenServices

DefaultTokenServices令牌服务是AuthorizationServerTokenServices接口的默认实现,位于org.springframework.security.oauth2.provider.token包内,提供了默认的操作令牌的方法,常用的有:

  • createAccessToken:根据客户端信息、登录用户信息来创建请求令牌(access_token)以及刷新令牌(refresh_token
  • refreshAccessToken:根据刷新令牌(refresh_token)来获取一个全新的请求令牌(access_token
  • revokeToken:撤销令牌,删除用户生成的请求令牌(access_token)、刷新令牌(refresh_token

源码解析:生成令牌

DefaultTokenServices#createAccessToken:

@Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
            if (!existingAccessToken.isExpired()) {
                this.tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }

            if (existingAccessToken.getRefreshToken() != null) {
                refreshToken = existingAccessToken.getRefreshToken();
                this.tokenStore.removeRefreshToken(refreshToken);
            }

            this.tokenStore.removeAccessToken(existingAccessToken);
        }

        if (refreshToken == null) {
            refreshToken = this.createRefreshToken(authentication);
        } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = this.createRefreshToken(authentication);
            }
        }

        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
        this.tokenStore.storeAccessToken(accessToken, authentication);
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            this.tokenStore.storeRefreshToken(refreshToken, authentication);
        }

        return accessToken;
    }

在创建令牌的源码方法中,首先根据认证信息去读取存储介质(TokenStore实现类)内该账户的令牌,如果令牌已经存储并且并未过期,则直接返回(这也就是同一个账户不同人登录时返回同一个令牌的逻辑),如果令牌已经过期,则删除刷新令牌(refresh_token)、请求令牌(access_token)后重新生成。

源码解析:刷新令牌

DefaultTokenServices#refreshAccessToken:

@Transactional(
        noRollbackFor = {InvalidTokenException.class, InvalidGrantException.class}
    )
    public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
        if (!this.supportRefreshToken) {
            throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
        } else {
            OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
            if (refreshToken == null) {
                throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
            } else {
                OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
                if (this.authenticationManager != null && !authentication.isClientOnly()) {
                    Authentication userAuthentication = authentication.getUserAuthentication();
                    PreAuthenticatedAuthenticationToken preAuthenticatedToken = new PreAuthenticatedAuthenticationToken(userAuthentication, "", authentication.getAuthorities());
                    if (userAuthentication.getDetails() != null) {
                        preAuthenticatedToken.setDetails(userAuthentication.getDetails());
                    }

                    Authentication user = this.authenticationManager.authenticate(preAuthenticatedToken);
                    Object details = authentication.getDetails();
                    authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
                    authentication.setDetails(details);
                }

                String clientId = authentication.getOAuth2Request().getClientId();
                if (clientId != null && clientId.equals(tokenRequest.getClientId())) {
                    this.tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
                    if (this.isExpired(refreshToken)) {
                        this.tokenStore.removeRefreshToken(refreshToken);
                        throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
                    } else {
                        authentication = this.createRefreshedAuthentication(authentication, tokenRequest);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.removeRefreshToken(refreshToken);
                            refreshToken = this.createRefreshToken(authentication);
                        }

                        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
                        this.tokenStore.storeAccessToken(accessToken, authentication);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
                        }

                        return accessToken;
                    }
                } else {
                    throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
                }
            }
        }
    }

在刷新令牌的源码方法中,首先需要读取刷新令牌(refresh_token)的具体内容,如果不存在则直接抛出刷新令牌无效的异常InvalidGrantException

执行令牌刷新之前,需要根据刷新令牌删除请求令牌removeAccessTokenUsingRefreshToken,删除后再次判定刷新令牌是否失效,如果失效抛出InvalidTokenException异常。

刷新令牌的重复使用是根据全局变量reuseRefreshToken来判定的,默认情况下该变量的值为true,也就是刷新令牌可以重复使用,但是经过createAccessToken > TokenEnhancer#enhance处理后刷新令牌会被重新创建并替换(这个地方貌似是一个Bug)。

重写TokenServices

期望效果

假设请求令牌(access_token)的有效期为7200秒,也就是2个小时,刷新令牌(refresh_token)的有效期为43200秒,也就是12个小时。

在第一次通过createAccessToken获取令牌后,每次请求令牌(access_token)过期后通过刷新的方式(/oauth/token?grant_type=refresh_token)重新获取一次新的(有效期为2个小时)请求令牌,当刷新令牌(refresh_token)失效后,再次通过createAccessToken方法来获取令牌。

分析期望效果

针对上面的期望效果我们需要修改createAccessTokenrefreshAccessToken两个方法的源码,调用createAccessToken方法时不再判定是否使用已经存在的有效令牌,而调用refreshAccessToken方法时需要删除响应的refresh_token的返回字段并把新的请求令牌与刷新令牌进行绑定。

OverrideTokenServices

复制DefaultTokenServices类内的全部代码,创建一个名为OverrideTokenServices的类,为了兼容原来的逻辑,需要添加一个全局变量alwaysCreateToken,用于判定是否始终创建令牌。

重写创建令牌逻辑

@Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2RefreshToken refreshToken = null;
        OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
        // 根据alwaysCreateToken字段判定是否始终创建令牌
        if (!this.alwaysCreateToken && existingAccessToken != null) {
            if (!existingAccessToken.isExpired()) {
                this.tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }

            if (existingAccessToken.getRefreshToken() != null) {
                refreshToken = existingAccessToken.getRefreshToken();
                this.tokenStore.removeRefreshToken(refreshToken);
            }

            this.tokenStore.removeAccessToken(existingAccessToken);
        }
        if (refreshToken == null) {
            refreshToken = this.createRefreshToken(authentication);
        } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = this.createRefreshToken(authentication);
            }
        }
        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
        this.tokenStore.storeAccessToken(accessToken, authentication);
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            this.tokenStore.storeRefreshToken(refreshToken, authentication);
        }

        return accessToken;
    }

如果我们想使用原来的逻辑,在初始化OverrideTokenServices类时需要设置alwaysCreateToken变量的值为false

重写刷新令牌逻辑

public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
        if (!this.supportRefreshToken) {
            throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
        } else {
            OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
            if (refreshToken == null) {
                throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
            } else {
                OAuth2Authentication authentication = this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
                if (this.authenticationManager != null && !authentication.isClientOnly()) {
                    Authentication userAuthentication = authentication.getUserAuthentication();
                    PreAuthenticatedAuthenticationToken preAuthenticatedToken = new PreAuthenticatedAuthenticationToken(userAuthentication, "", authentication.getAuthorities());
                    if (userAuthentication.getDetails() != null) {
                        preAuthenticatedToken.setDetails(userAuthentication.getDetails());
                    }

                    Authentication user = this.authenticationManager.authenticate(preAuthenticatedToken);
                    Object details = authentication.getDetails();
                    authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
                    authentication.setDetails(details);
                }

                String clientId = authentication.getOAuth2Request().getClientId();
                if (clientId != null && clientId.equals(tokenRequest.getClientId())) {
                    this.tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
                    if (this.isExpired(refreshToken)) {
                        this.tokenStore.removeRefreshToken(refreshToken);
                        throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
                    } else {
                        authentication = this.createRefreshedAuthentication(authentication, tokenRequest);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.removeRefreshToken(refreshToken);
                            refreshToken = this.createRefreshToken(authentication);
                        }

                        DefaultOAuth2AccessToken accessToken = (DefaultOAuth2AccessToken) this.createAccessToken(authentication, refreshToken);
                        // If you reuse the refresh token, set the refresh token to the new AccessToken
                        // 如果重复使用刷新令牌,将刷新令牌与新生成的请求令牌进行绑定
                        if (this.reuseRefreshToken) {
                            accessToken.setRefreshToken(refreshToken);
                        }
                        this.tokenStore.storeAccessToken(accessToken, authentication);
                        if (!this.reuseRefreshToken) {
                            this.tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
                        }
                        // No new token will be returned after refresh
                        // 刷新令牌后不再返回refresh_token
                        accessToken.setRefreshToken(null);
                        return accessToken;
                    }
                } else {
                    throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
                }
            }
        }
    }

DefaultTokenServices类中默认定义了全局变量reuseRefreshToken,该变量的值为true,表示默认情况下刷新令牌(refresh_token)是可以重复使用的,一般刷新令牌的过期时间都比较久,当请求令牌(access_token)失效后根据刷新令牌进行获取新的有效请求令牌。

配置TokenServices

我们需要在AuthorizationServerConfigurerAdapter实现类内进行配置TokenServices的替换使用,如下所示:

/**
  * 实例化{@link OverrideTokenServices}
  *
  * @return {@link OverrideTokenServices}
  */
private AuthorizationServerTokenServices tokenServices() {
  OverrideTokenServices tokenServices = new OverrideTokenServices();
  tokenServices.setTokenStore(tokenStore());
  tokenServices.setAlwaysCreateToken(true);
  tokenServices.setSupportRefreshToken(true);
  tokenServices.setClientDetailsService(clientDetailsService);
  return tokenServices;
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  endpoints
    .authenticationManager(authenticationManager)
    .tokenStore(tokenStore())
    // 配置替换使用TokenServices
    .tokenServices(tokenServices());
}

测试

获取令牌示例:

第一次获取令牌:
yuqiyu@hengyu ~> curl -X POST -u "local:123456" http://localhost:9091/oauth/token -d "grant_type=password&username=hengboy&password=123456" | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   199    0   147  100    52    362    128 --:--:-- --:--:-- --:--:--   491
{
  "access_token": "qoL7Kg33-deYw-aw8PnIKK-qxEk",
  "token_type": "bearer",
  "refresh_token": "-OfFqllKZJC6-r_v_uR9KGUBXl0",
  "expires_in": 7199,
  "scope": "read"
}
第二次获取令牌:
yuqiyu@hengyu ~> curl -X POST -u "local:123456" http://localhost:9091/oauth/token -d "grant_type=password&username=hengboy&password=123456" | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   199    0   147  100    52    896    317 --:--:-- --:--:-- --:--:--  1213
{
  "access_token": "hfo01xMTVE1xxxbzQLY7vPfLXPE",
  "token_type": "bearer",
  "refresh_token": "QuLgm-H3xHzo71M_XSLrglsRs_o",
  "expires_in": 7199,
  "scope": "read"
}

可以看到上面使用同一个账号获取了两次令牌,而这两次的令牌内容是完全不同的,这也就是实现了针对同一个账号不同人登录时返回新的令牌的需求。

刷新令牌示例:

根据第一次获取的刷新令牌刷新:
yuqiyu@hengyu ~> curl -X POST -u "local:123456" http://localhost:9091/oauth/token -d "grant_type=refresh_token&refresh_token=-OfFqllKZJC6-r_v_uR9KGUBXl0" | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   167    0   101  100    66   1109    725 --:--:-- --:--:-- --:--:--  1835
{
  "access_token": "KuOprmzBCzC78NXlTkHvZGs9rhs",
  "token_type": "bearer",
  "expires_in": 7199,
  "scope": "read"
}
根据第二次获取的刷新令牌刷新:
yuqiyu@hengyu ~> curl -X POST -u "local:123456" http://localhost:9091/oauth/token -d "grant_type=refresh_token&refresh_token=QuLgm-H3xHzo71M_XSLrglsRs_o" | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   167    0   101  100    66   1122    733 --:--:-- --:--:-- --:--:--  1855
{
  "access_token": "aLPOEkfUCxn87XkTkcwyixaUO1s",
  "token_type": "bearer",
  "expires_in": 7200,
  "scope": "read"
}

同一个账户,上面虽然刷新了两次,但是令牌的有效期不会相互影响,第一次刷新使用的是第一次获取的刷新令牌,这样其实也就是刷新的第一次的请求令牌,与第二次的无关!!!

代码示例

如果您喜欢本篇文章请为源码仓库点个Star,谢谢!!!
本篇文章示例源码可以通过以下途径获取,目录为oauth2-always-create-token

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

推荐阅读更多精彩内容