You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor 8 InteractiveWebAssembly模式下JWT跨端同步更新问题

解决Blazor 8 WASM客户端令牌刷新后服务端Cookie同步问题

针对你遇到的客户端刷新令牌后,服务端Cookie未更新导致刷新令牌重复使用失败的问题,以下是两种可行的解决方案:


方案一:客户端刷新令牌后主动同步服务端Cookie

核心思路是客户端完成令牌刷新后,立即调用服务端API,将新令牌同步到服务端的认证Cookie中,确保两端令牌状态一致。

1. 服务端添加令牌同步API

在服务端创建一个API端点,接收客户端传来的新令牌,重新生成认证Cookie:

[ApiController]
[Route("api/auth")]
[Authorize]
public class AuthSyncController : ControllerBase
{
    private readonly IAuthenticationService _authService;
    private const string AuthScheme = IdentityConstants.ApplicationScheme;

    public AuthSyncController(IAuthenticationService authService)
    {
        _authService = authService;
    }

    [HttpPost("sync-tokens")]
    public async Task<IActionResult> SyncTokens([FromBody] TokenSyncModel model)
    {
        // 解析新访问令牌的用户Claims(确保令牌合法性)
        var jwtHandler = new JwtSecurityTokenHandler();
        var jwtToken = jwtHandler.ReadJwtToken(model.AccessToken);
        var claims = jwtToken.Claims.ToList();

        // 添加新令牌到Claims集合
        claims.Add(new Claim("access_token", model.AccessToken));
        claims.Add(new Claim("refresh_token", model.RefreshToken));

        // 构建新的认证主体并更新Cookie
        var newIdentity = new ClaimsIdentity(claims, AuthScheme);
        var newPrincipal = new ClaimsPrincipal(newIdentity);
        
        await _authService.SignInAsync(HttpContext, AuthScheme, newPrincipal,
            new AuthenticationProperties { IsPersistent = true });

        return Ok();
    }
}

public class TokenSyncModel
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
}

2. 客户端刷新令牌后调用同步API

修改客户端PersistentAuthenticationStateProvider的令牌刷新逻辑,成功获取新令牌后立即调用服务端同步接口:

public class ClientAuthStateProvider : PersistentAuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    private readonly IAuthApiService _authApi;
    private Task<AuthenticationState> _authStateTask;

    public ClientAuthStateProvider(HttpClient httpClient, IAuthApiService authApi)
    {
        _httpClient = httpClient;
        _authApi = authApi;
        _authStateTask = base.GetAuthenticationStateAsync();
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var authState = await _authStateTask;
        var user = authState.User;

        if (user.Identity.IsAuthenticated && IsTokenExpired(user.FindFirst("access_token")?.Value))
        {
            var oldRefreshToken = user.FindFirst("refresh_token")?.Value;
            // 调用外部API获取新令牌
            var newTokens = await _authApi.RefreshTokenAsync(oldRefreshToken);

            // 构建新的认证主体
            var jwtHandler = new JwtSecurityTokenHandler();
            var jwtToken = jwtHandler.ReadJwtToken(newTokens.AccessToken);
            var claims = jwtToken.Claims.ToList();
            claims.Add(new Claim("access_token", newTokens.AccessToken));
            claims.Add(new Claim("refresh_token", newTokens.RefreshToken));
            
            var newIdentity = new ClaimsIdentity(claims, IdentityConstants.ApplicationScheme);
            var newPrincipal = new ClaimsPrincipal(newIdentity);

            // 更新客户端本地认证状态
            _authStateTask = Task.FromResult(new AuthenticationState(newPrincipal));

            // 同步到服务端更新Cookie
            try
            {
                await _httpClient.PostAsJsonAsync("/api/auth/sync-tokens",
                    new TokenSyncModel { AccessToken = newTokens.AccessToken, RefreshToken = newTokens.RefreshToken });
            }
            catch
            {
                // 同步失败时强制登出,避免状态不一致
                await SignOutAsync();
                _authStateTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal()));
            }
        }

        return await _authStateTask;
    }

    private bool IsTokenExpired(string token)
    {
        // 实现JWT过期校验逻辑
        if (string.IsNullOrEmpty(token)) return true;
        var jwtHandler = new JwtSecurityTokenHandler();
        var jwtToken = jwtHandler.ReadJwtToken(token);
        return jwtToken.ValidTo <= DateTime.UtcNow;
    }

    private async Task SignOutAsync()
    {
        await _httpClient.PostAsync("/Account/Logout", null);
    }
}

方案二:服务端刷新失败时触发状态重置

修改服务端PersistingServerAuthenticationStateProvider的刷新逻辑,当检测到刷新令牌无效(已被客户端使用)时,直接登出当前用户,强制客户端重新同步状态:

public class ServerAuthStateProvider : PersistingServerAuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuthenticationService _authService;
    private readonly IAuthApiService _authApi;

    public ServerAuthStateProvider(IHttpContextAccessor httpContextAccessor, 
        IAuthenticationService authService, IAuthApiService authApi)
    {
        _httpContextAccessor = httpContextAccessor;
        _authService = authService;
        _authApi = authApi;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var authState = await base.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity.IsAuthenticated && IsTokenExpired(user.FindFirst("access_token")?.Value))
        {
            var refreshToken = user.FindFirst("refresh_token")?.Value;
            try
            {
                var newTokens = await _authApi.RefreshTokenAsync(refreshToken);
                // 构建新认证主体并更新状态
                var newPrincipal = BuildPrincipalFromTokens(newTokens);
                await _authService.SignInAsync(_httpContextAccessor.HttpContext, 
                    IdentityConstants.ApplicationScheme, newPrincipal);
                return new AuthenticationState(newPrincipal);
            }
            catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
            {
                // 刷新令牌无效,说明客户端已使用,直接登出
                await _authService.SignOutAsync(_httpContextAccessor.HttpContext, 
                    IdentityConstants.ApplicationScheme);
                return new AuthenticationState(new ClaimsPrincipal());
            }
        }

        return authState;
    }

    private ClaimsPrincipal BuildPrincipalFromTokens(TokenResponse tokens)
    {
        // 实现从令牌构建ClaimsPrincipal的逻辑
        var jwtHandler = new JwtSecurityTokenHandler();
        var jwtToken = jwtHandler.ReadJwtToken(tokens.AccessToken);
        var claims = jwtToken.Claims.ToList();
        claims.Add(new Claim("access_token", tokens.AccessToken));
        claims.Add(new Claim("refresh_token", tokens.RefreshToken));
        
        return new ClaimsPrincipal(new ClaimsIdentity(claims, IdentityConstants.ApplicationScheme));
    }

    private bool IsTokenExpired(string token)
    {
        // 同客户端的过期校验逻辑
        if (string.IsNullOrEmpty(token)) return true;
        var jwtHandler = new JwtSecurityTokenHandler();
        var jwtToken = jwtHandler.ReadJwtToken(token);
        return jwtToken.ValidTo <= DateTime.UtcNow;
    }
}

关键注意事项

  1. 令牌合法性校验:服务端同步API需验证传入令牌的有效性,避免恶意请求篡改令牌。
  2. 异常处理:同步失败或刷新失败时,需强制登出用户,避免两端状态不一致引发更多问题。
  3. 认证Scheme统一:确保客户端和服务端使用相同的认证Scheme(如IdentityConstants.ApplicationScheme)。

内容的提问来源于stack exchange,提问作者GLuca74

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 20:26:19