赞
踩
最近项目组同事反应,系统经常在使用的过程中弹出会话超时提示。我觉得这个问题属实有点狗,平台设计之初保存了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去刷新,这样问题就解决了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。