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

Blazor Server客户端JWT令牌通用处理方案求助

Blazor Server 通用JWT令牌处理方案修复指南

我已使用JWT令牌保护Web API,认证配置运行正常,但找不到Blazor Server客户端处理JWT令牌的相关指南。希望采用通用方式处理,避免在每个Razor组件中直接调用受保护API,因此编写了AuthClientService、TokenHandler并在Program.cs中配置,但方案无效,恳请提供可行的客户端JWT令牌通用处理方法。

我尝试的代码

AuthClientService.cs

public class AuthClientService
{
    private readonly HttpClient httpClient;
    private readonly Blazored.LocalStorage.ILocalStorageService localStorage;
    private string token;
    private DateTime tokenExpiration;
    private const string TokenKey = "auth_token";
    public AuthClientService(HttpClient httpClient, Blazored.LocalStorage.ILocalStorageService localStorage)
    {
        this.httpClient = httpClient;
        this.localStorage = localStorage;
    }
    public async Task<string> GetTokenAsync()
    {
        var token = await localStorage.GetItemAsStringAsync(TokenKey);
        // Check if the token is null or expired
        if (string.IsNullOrEmpty(token))//|| IsTokenExpired(token))
        {
            // Request a new token
            token = await RequestNewToken();
            // Save the token to local storage
            await localStorage.SetItemAsync(TokenKey, token);
        }
        return token;
    }
    private async Task<string> RequestNewToken()
    {
        var user = new User("hams@email.com", "hams");
        var response = await httpClient.PostAsJsonAsync("api/auth", user);
        if (response.IsSuccessStatusCode)
        {
            var token = await response.Content.ReadAsStringAsync();
            var jwtToken = token.Trim('"');
            // Parse the token and extract the expiration date
            var tokenHandler = new JwtSecurityTokenHandler();
            var parsedToken = tokenHandler.ReadJwtToken(jwtToken);
            tokenExpiration = parsedToken.ValidTo;
            return await response.Content.ReadAsStringAsync();
        }
        else
        {
            return null;
        }
    }
}

TokenHandler.cs

public class TokenHandler : DelegatingHandler
{
    private readonly AuthClientService authService;
    public TokenHandler(AuthClientService authService)
    {
        this.authService = authService;
    }
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await authService.GetTokenAsync();
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
        return await base.SendAsync(request, cancellationToken);
    }
}

Program.cs

builder.Services.AddBlazoredLocalStorage();
builder.Services.AddScoped(async sp =>
{
var httpClient = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) };
// Add the TokenHandler to the HttpClient's pipeline
var authClientService = sp.GetRequiredService<AuthClientService>();
var tokenHandler = new TokenHandler(authClientService);
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", await authClientService.GetTokenAsync());
return httpClient;
});

问题分析与修复方案

你的方案无效核心原因有三点:

  1. HttpClient未正确集成TokenHandler:未将自定义处理器加入请求管道,导致令牌无法自动附加
  2. 依赖循环问题:AuthClientService依赖HttpClient,注册HttpClient时又依赖AuthClientService,导致注入失败
  3. 令牌过期检查缺失:注释了过期校验逻辑,无法自动刷新失效令牌

修复后的完整实现

1. 完善AuthClientService(添加缓存与过期校验)

public class AuthClientService
{
    private readonly HttpClient _httpClient;
    private readonly Blazored.LocalStorage.ILocalStorageService _localStorage;
    private string _cachedToken;
    private DateTime _tokenExpiration;
    private const string TokenKey = "auth_token";

    public AuthClientService(HttpClient httpClient, Blazored.LocalStorage.ILocalStorageService localStorage)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
    }

    public async Task<string> GetValidTokenAsync()
    {
        // 优先使用缓存的有效令牌(提前1分钟刷新避免边缘过期)
        if (!string.IsNullOrEmpty(_cachedToken) && DateTime.UtcNow < _tokenExpiration.AddMinutes(-1))
        {
            return _cachedToken;
        }

        // 从本地存储读取令牌并校验有效期
        var storedToken = await _localStorage.GetItemAsStringAsync(TokenKey);
        if (!string.IsNullOrEmpty(storedToken))
        {
            var pureToken = storedToken.Trim('"');
            var parsedToken = new JwtSecurityTokenHandler().ReadJwtToken(pureToken);
            _tokenExpiration = parsedToken.ValidTo;
            
            if (DateTime.UtcNow < _tokenExpiration.AddMinutes(-1))
            {
                _cachedToken = pureToken;
                return _cachedToken;
            }
        }

        // 请求新令牌并更新缓存与本地存储
        var newToken = await RequestNewToken();
        if (!string.IsNullOrEmpty(newToken))
        {
            _cachedToken = newToken;
            await _localStorage.SetItemAsync(TokenKey, $"\"{newToken}\"");
            return newToken;
        }

        throw new UnauthorizedAccessException("无法获取有效认证令牌");
    }

    private async Task<string> RequestNewToken()
    {
        var user = new User("hams@email.com", "hams");
        var response = await _httpClient.PostAsJsonAsync("api/auth", user);
        
        if (response.IsSuccessStatusCode)
        {
            var tokenString = await response.Content.ReadAsStringAsync();
            var pureToken = tokenString.Trim('"');
            var parsedToken = new JwtSecurityTokenHandler().ReadJwtToken(pureToken);
            _tokenExpiration = parsedToken.ValidTo;
            return pureToken;
        }

        return null;
    }
}

2. 调整TokenHandler(解决依赖循环)

通过IServiceProvider延迟解析服务,避免循环依赖,同时添加401重试逻辑:

public class TokenHandler : DelegatingHandler
{
    private readonly IServiceProvider _serviceProvider;

    public TokenHandler(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        using var scope = _serviceProvider.CreateScope();
        var authService = scope.ServiceProvider.GetRequiredService<AuthClientService>();
        
        var token = await authService.GetValidTokenAsync();
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }

        var response = await base.SendAsync(request, cancellationToken);

        // 令牌过期返回401时,刷新令牌并重试一次
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            var newToken = await authService.GetValidTokenAsync();
            if (!string.IsNullOrEmpty(newToken))
            {
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
                response = await base.SendAsync(request, cancellationToken);
            }
        }

        return response;
    }
}

3. 正确注册服务到Program.cs

builder.Services.AddBlazoredLocalStorage();

// 先注册AuthClientService
builder.Services.AddScoped<AuthClientService>();

// 注册TokenHandler
builder.Services.AddTransient<TokenHandler>();

// 配置HttpClient并集成TokenHandler
builder.Services.AddScoped(sp =>
{
    var tokenHandler = sp.GetRequiredService<TokenHandler>();
    tokenHandler.InnerHandler = new HttpClientHandler();
    
    var httpClient = new HttpClient(tokenHandler)
    {
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    };
    
    return httpClient;
});

使用方式

在Razor组件或服务中直接注入HttpClient即可,所有请求会自动附加JWT令牌:

@inject HttpClient Http

@code {
    protected override async Task OnInitializedAsync()
    {
        var data = await Http.GetFromJsonAsync<MyData>("api/protected/data");
        // 处理返回数据
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:06:13