Oauth刷新令牌失效,InvalidGrantException问题

问题描述

最近项目组同事反应,系统经常在使用的过程中弹出会话超时提示。我觉得这个问题属实有点狗,平台设计之初保存了refresh_token,不仅开发了定时刷新令牌的机制,也对这个机制进行了反复测试,只要access_token快到期了就会取回新的令牌,运行非常顺畅。

不过有问题还是要解决。先翻一遍日志,看见这么个东西:

2023-11-06 11:33:42.222 [http-nio-9101-exec-4] ERRORc.xxx.oauth.client.api.local.OauthLocalApi-400 : [<InvalidGrantException><error>invalid_grant</error><error_description>Invalid refresh token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiIxNCIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI1YmRmMGVlYi0zN2... (470 bytes)]
org.springframework.web.client.HttpClientErrorException$BadRequest: 400 : [<InvalidGrantException><error>invalid_grant</error><error_description>Invalid refresh token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiIxNCIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI1YmRmMGVlYi0zN2... (470 bytes)]
	at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:101)
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:170)
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:112)
	at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
	at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:782)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:740)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:674)
	at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:583)
	……
	……
	……

显然,调用oauth/token接口刷新令牌时,oauth抛出了异常,提示我们传入的refreshToken是非法的。复测该问题,在测试系统登录后用refreshToken调用oauth/token接口,发现接口可以正常返回新的令牌信息,没有抛出上面的异常。

排查

没有简洁的办法,又开始了扒代码大法。顺着提示信息,找到了DefaultTokenServices类,其中这段代码是刷新令牌的逻辑:

@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
	public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
			throws AuthenticationException {

		if (!supportRefreshToken) {
			throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
		}

		OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
		if (refreshToken == null) {
			throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
		}

		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);
		}
		String clientId = authentication.getOAuth2Request().getClientId();
		if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
			throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
		}

		// clear out any access tokens already associated with the refresh
		// token.
		tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);

		if (isExpired(refreshToken)) {
			tokenStore.removeRefreshToken(refreshToken);
			throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
		}

		authentication = createRefreshedAuthentication(authentication, tokenRequest);

		if (!reuseRefreshToken) {
			tokenStore.removeRefreshToken(refreshToken);
			refreshToken = createRefreshToken(authentication);
		}

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

可以看到方法开始的部分,有两项对refreshToken的验证,第一个是验证当前环境是否支持刷新令牌,第二个是验证传入的refreshToken是否合法。debug这段代码好几次,终于发现了问题:
第一次进入这个方法,refreshToken可以验证通过,且生成新的access_token和refresh_token;
使用新的refresh_token第二次进入这个方法,发现tokenStore.readRefreshToken(refreshTokenValue)返回null,验证不通过。
debug跟踪进入tokenStore.readRefreshToken(refreshTokenValue)方法,发现refreshTokenStore中没有我们使用的新的refresh_token,只有第一次登录时使用的refresh_token。
在这里插入图片描述
此时我们基本确定了问题方向:第一次刷新令牌之后,oauth返回了一个新的refresh_token,但是这个refresh_token并没有存到refreshTokenStore中。

结论

回到上面的DefaultTokenServices代码中,可见

		if (!reuseRefreshToken) {
			tokenStore.removeRefreshToken(refreshToken);
			refreshToken = createRefreshToken(authentication);
		}

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

这里有个reuseRefreshToken配置,默认为true,此时可以重复使用refreshToken,而且新生成的refresh_token不会被存入refreshTokenStore

找到问题后,回复前端小伙伴,刷新令牌以后,不要将服务器返回的新refresh_token保存起来,而是一直重复使用一开始的refresh_token去刷新,这样问题就解决了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值