JWT 令牌问题同时使用 Refresh 令牌刷新
Issue with JWT token multiple simultaneously refresh with Refresh token
工具:带有 EF Core 的 .NET 6、带有 Axios 的 Vue 3。
R-Token 是刷新令牌。 DB是数据库。
我有 JWT + 刷新令牌认证的简单实现。
- 客户端发送登录名和密码。
- 检查数据库中的密码哈希值。
- 如果成功,生成 JWT 令牌(生命周期短,1-5 分钟)并刷新
保存到数据库的令牌(长寿命,365 天)。
- 客户端使用 JWT 发出请求。
- 当 Axios 拦截器得到 401 时,然后尝试刷新令牌
在刷新令牌下方生成。
- 使用的刷新令牌从数据库中删除,如果应用程序在数据库中找不到 R-令牌,它会响应 403。
所以,在客户端,我有一些调用服务器的间隔操作。有时,它们会同时执行,如果 JWT 令牌过期,我几乎没有收到使用相同 R 令牌同时刷新令牌的请求。
问题是:
在这种情况下,第一个请求删除 R-token 并生成新的,然后下一个请求将失败。
这个问题我该怎么办?
我的看法:
- 在 Axios 拦截器中做类似单例的事情。
- 以某种方式在后端控制器中使用 .NET 锁构造,但用于单独的客户端。
请帮忙。
Axios 拦截器:
instance.interceptors.response.use(response => response,
async (error) => {
const status = error.response ? error.response.status : undefined
const originalRequest = error.config
if(status === 401) {
originalRequest._retry = true
let tryRefresh = await store.dispatch('auth/TryRefreshToken')
if(tryRefresh === false) {
store.dispatch('auth/Logout')
return Promise.reject(error)
}
originalRequest.headers['Authorization'] = 'Bearer ' + store.getters['auth/auth'].accessToken
return instance(originalRequest)
}
if (status === undefined)
{
return Promise.reject(error)
}
return Promise.reject(error)
}
)
.NET 控制器中的刷新令牌操作:
[HttpPost, Route("Refresh/{refreshToken}")]
[ProducesResponseType(typeof(AuthenticationResponse), 200)]
public IActionResult RefreshTokens(string refreshToken)
{
Request.Headers.TryGetValue("Authorization", out var accessTokenHeader);
string? accessToken = accessTokenHeader.FirstOrDefault()?.Replace("Bearer", string.Empty).Trim();
if (string.IsNullOrEmpty(accessToken)) return BadRequest("No access token presented.");
JwtSecurityToken? expiredToken = new JwtSecurityTokenHandler().ReadToken(accessToken) as JwtSecurityToken;
if (expiredToken is null) return BadRequest("Bad access token format");
IEnumerable<Claim> claims = expiredToken.Claims;
if (int.TryParse(claims.FirstOrDefault(x => x.Type == "User:Id")?.Value, out int userId) is false)
return BadRequest("No user id in token presented");
User? user = _mainContext.Users.AsNoTrackingWithIdentityResolution()
.Include(x => x.Roles)
.FirstOrDefault(x => x.Id == userId);
if (user is null) return NotFound("No user found");
var userDto = user.ToUserDto();
if (_refreshTokenManager.IsTokenValid(refreshToken, user.Id) is false)
return StatusCode(403);
try {
_refreshTokenManager.RemoveToken(refreshToken);
}
catch (Exception ex) {
Log.Error(ex, "Error in used refresh token deletion.");
}
JwtSettingsDto jwtSettings = _configuration.GetSection("Authorization:Jwt").Get<JwtSettingsDto>();
string newAccessToken = _tokenGeneratorService.GenerateAccessJwtToken(userDto, jwtSettings);
string newRefreshToken = _refreshTokenManager.CreateToken(userDto, Request);
return Ok(new AuthenticationResponse(newAccessToken, newRefreshToken, user.Login, user.DisplayName));
}
当刷新令牌一次性使用时(最近推荐),客户端有责任同步令牌刷新。如果多个视图同时调用 API,UI 需要这样做。
在实用程序 class 中执行此操作相当容易,该实用程序将承诺用于令牌刷新,然后仅对其中的第一个进行实际的 HTTP 调用,然后 returns 相同的结果所有请求。
有关示例,请参阅 this ConcurrentActionHandler class of mine, used in a React SPA, which is called from this API client code。
在您的情况下,Michael Levy 发布的拦截器 class 看起来做同样的工作。所以这可能是最好的选择。不过,相同的设计模式可以应用于其他类型的客户端,例如使用 Swift 或 Kotlin 编码的移动应用程序。
工具:带有 EF Core 的 .NET 6、带有 Axios 的 Vue 3。
R-Token 是刷新令牌。 DB是数据库。
我有 JWT + 刷新令牌认证的简单实现。
- 客户端发送登录名和密码。
- 检查数据库中的密码哈希值。
- 如果成功,生成 JWT 令牌(生命周期短,1-5 分钟)并刷新 保存到数据库的令牌(长寿命,365 天)。
- 客户端使用 JWT 发出请求。
- 当 Axios 拦截器得到 401 时,然后尝试刷新令牌 在刷新令牌下方生成。
- 使用的刷新令牌从数据库中删除,如果应用程序在数据库中找不到 R-令牌,它会响应 403。
所以,在客户端,我有一些调用服务器的间隔操作。有时,它们会同时执行,如果 JWT 令牌过期,我几乎没有收到使用相同 R 令牌同时刷新令牌的请求。
问题是: 在这种情况下,第一个请求删除 R-token 并生成新的,然后下一个请求将失败。
这个问题我该怎么办?
我的看法:
- 在 Axios 拦截器中做类似单例的事情。
- 以某种方式在后端控制器中使用 .NET 锁构造,但用于单独的客户端。
请帮忙。
Axios 拦截器:
instance.interceptors.response.use(response => response,
async (error) => {
const status = error.response ? error.response.status : undefined
const originalRequest = error.config
if(status === 401) {
originalRequest._retry = true
let tryRefresh = await store.dispatch('auth/TryRefreshToken')
if(tryRefresh === false) {
store.dispatch('auth/Logout')
return Promise.reject(error)
}
originalRequest.headers['Authorization'] = 'Bearer ' + store.getters['auth/auth'].accessToken
return instance(originalRequest)
}
if (status === undefined)
{
return Promise.reject(error)
}
return Promise.reject(error)
}
)
.NET 控制器中的刷新令牌操作:
[HttpPost, Route("Refresh/{refreshToken}")]
[ProducesResponseType(typeof(AuthenticationResponse), 200)]
public IActionResult RefreshTokens(string refreshToken)
{
Request.Headers.TryGetValue("Authorization", out var accessTokenHeader);
string? accessToken = accessTokenHeader.FirstOrDefault()?.Replace("Bearer", string.Empty).Trim();
if (string.IsNullOrEmpty(accessToken)) return BadRequest("No access token presented.");
JwtSecurityToken? expiredToken = new JwtSecurityTokenHandler().ReadToken(accessToken) as JwtSecurityToken;
if (expiredToken is null) return BadRequest("Bad access token format");
IEnumerable<Claim> claims = expiredToken.Claims;
if (int.TryParse(claims.FirstOrDefault(x => x.Type == "User:Id")?.Value, out int userId) is false)
return BadRequest("No user id in token presented");
User? user = _mainContext.Users.AsNoTrackingWithIdentityResolution()
.Include(x => x.Roles)
.FirstOrDefault(x => x.Id == userId);
if (user is null) return NotFound("No user found");
var userDto = user.ToUserDto();
if (_refreshTokenManager.IsTokenValid(refreshToken, user.Id) is false)
return StatusCode(403);
try {
_refreshTokenManager.RemoveToken(refreshToken);
}
catch (Exception ex) {
Log.Error(ex, "Error in used refresh token deletion.");
}
JwtSettingsDto jwtSettings = _configuration.GetSection("Authorization:Jwt").Get<JwtSettingsDto>();
string newAccessToken = _tokenGeneratorService.GenerateAccessJwtToken(userDto, jwtSettings);
string newRefreshToken = _refreshTokenManager.CreateToken(userDto, Request);
return Ok(new AuthenticationResponse(newAccessToken, newRefreshToken, user.Login, user.DisplayName));
}
当刷新令牌一次性使用时(最近推荐),客户端有责任同步令牌刷新。如果多个视图同时调用 API,UI 需要这样做。
在实用程序 class 中执行此操作相当容易,该实用程序将承诺用于令牌刷新,然后仅对其中的第一个进行实际的 HTTP 调用,然后 returns 相同的结果所有请求。
有关示例,请参阅 this ConcurrentActionHandler class of mine, used in a React SPA, which is called from this API client code。
在您的情况下,Michael Levy 发布的拦截器 class 看起来做同样的工作。所以这可能是最好的选择。不过,相同的设计模式可以应用于其他类型的客户端,例如使用 Swift 或 Kotlin 编码的移动应用程序。