问题描述
最近项目组同事反应,系统经常在使用的过程中弹出会话超时提示。我觉得这个问题属实有点狗,平台设计之初保存了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去刷新,这样问题就解决了。