JWT 令牌问题同时使用 Refresh 令牌刷新

Issue with JWT token multiple simultaneously refresh with Refresh token

工具:带有 EF Core 的 .NET 6、带有 Axios 的 Vue 3。

R-Token 是刷新令牌。 DB是数据库。

我有 JWT + 刷新令牌认证的简单实现。

  1. 客户端发送登录名和密码。
  2. 检查数据库中的密码哈希值。
  3. 如果成功,生成 JWT 令牌(生命周期短,1-5 分钟)并刷新 保存到数据库的令牌(长寿命,365 天)。
  4. 客户端使用 JWT 发出请求。
  5. 当 Axios 拦截器得到 401 时,然后尝试刷新令牌 在刷新令牌下方生成。
  6. 使用的刷新令牌从数据库中删除,如果应用程序在数据库中找不到 R-令牌,它会响应 403。

所以,在客户端,我有一些调用服务器的间隔操作。有时,它们会同时执行,如果 JWT 令牌过期,我几乎没有收到使用相同 R 令牌同时刷新令牌的请求。

问题是: 在这种情况下,第一个请求删除 R-token 并生成新的,然后下一个请求将失败。

这个问题我该怎么办?

我的看法:

  1. 在 Axios 拦截器中做类似单例的事情。
  2. 以某种方式在后端控制器中使用 .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 编码的移动应用程序。