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

.NET 8 Blazor自动渲染模式下自定义JWT认证的Token存储问题

Blazor自动渲染模式下Token跨服务端/客户端共享方案

问题背景

我基于Blazor搭建了服务端+客户端项目,服务端负责编写API,应用采用自动渲染模式(同时支持服务端渲染、客户端渲染页面)。已实现CustomAuthenticationStateProvider并在两端完成注册,但核心需求是让Token能在服务端和客户端同时被访问。

当前使用的CustomAuthenticationStateProvider代码如下:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

    public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = _httpContextAccessor.HttpContext.Request.Cookies["token"];
        if (string.IsNullOrEmpty(token))
            return new AuthenticationState(_anonymous);

        var tokenHandler = new JwtSecurityTokenHandler();
        var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
        var user = new ClaimsPrincipal(identity);
        return await Task.FromResult(new AuthenticationState(user));
    }

    public void UpdateAuthenticationState(string token)
    {
        if (string.IsNullOrEmpty(token))
        {
            _httpContextAccessor.HttpContext.Response.Cookies.Delete("token");
            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous)));
            return;
        }

        var tokenHandler = new JwtSecurityTokenHandler();
        var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
        var user = new ClaimsPrincipal(identity);

        var cookieOptions = new CookieOptions
        {
            Expires = DateTime.Now.AddHours(1),
            HttpOnly = true,
            SameSite = SameSiteMode.Strict,
            Secure = true
        };
        _httpContextAccessor.HttpContext.Response.Cookies.Append("token", token, cookieOptions);
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
    }
}

遇到的问题

  • 依赖IHttpContextAccessor时,客户端不存在该服务,报错:blazor.web.js:1 Error: One or more errors occurred. (Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor' while attempting to activate 'Client.CustomAuthenticationStateProvider'.)
  • 改用LocalStorage/SessionStorage存储Token,服务端会因JSRuntime交互报错(服务端无法直接操作浏览器存储)。

解决方案:基于Cookie的跨端兼容实现

核心思路是服务端直接操作Cookie,客户端通过JS读取/写入Cookie,同时让CustomAuthenticationStateProvider兼容两端运行环境。

1. 重构CustomAuthenticationStateProvider

通过可选注入IHttpContextAccessor和IJSRuntime,区分服务端/客户端逻辑:

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor? _httpContextAccessor;
    private readonly IJSRuntime? _jsRuntime;
    private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
    private readonly bool _isServerSide;

    // 构造函数兼容两端依赖注入
    public CustomAuthenticationStateProvider(
        IHttpContextAccessor? httpContextAccessor = null,
        IJSRuntime? jsRuntime = null)
    {
        _httpContextAccessor = httpContextAccessor;
        _jsRuntime = jsRuntime;
        _isServerSide = httpContextAccessor != null;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        string? token = null;

        if (_isServerSide)
        {
            // 服务端直接从Request Cookie读取Token
            token = _httpContextAccessor.HttpContext.Request.Cookies["token"];
        }
        else
        {
            // 客户端通过JS读取Cookie
            token = await _jsRuntime.InvokeAsync<string>("blazorGetCookie", "token");
        }

        if (string.IsNullOrEmpty(token))
            return new AuthenticationState(_anonymous);

        var tokenHandler = new JwtSecurityTokenHandler();
        var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public async Task UpdateAuthenticationState(string? token)
    {
        ClaimsPrincipal user;

        if (string.IsNullOrEmpty(token))
        {
            user = _anonymous;
            if (_isServerSide)
            {
                _httpContextAccessor.HttpContext.Response.Cookies.Delete("token");
            }
            else
            {
                await _jsRuntime.InvokeVoidAsync("blazorSetCookie", "token", "", -1);
            }
        }
        else
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
            user = new ClaimsPrincipal(identity);

            var expiresMinutes = 60;
            if (_isServerSide)
            {
                var cookieOptions = new CookieOptions
                {
                    Expires = DateTime.Now.AddMinutes(expiresMinutes),
                    HttpOnly = true,
                    SameSite = SameSiteMode.Strict,
                    Secure = true
                };
                _httpContextAccessor.HttpContext.Response.Cookies.Append("token", token, cookieOptions);
            }
            else
            {
                // 客户端通过JS设置Cookie
                await _jsRuntime.InvokeVoidAsync("blazorSetCookie", "token", token, expiresMinutes);
            }
        }

        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
    }
}

2. 添加客户端JS辅助方法

在客户端项目的wwwroot/index.html中添加操作Cookie的JS函数:

<script>
    // 读取指定名称的Cookie
    function blazorGetCookie(name) {
        let value = "; " + document.cookie;
        let parts = value.split("; " + name + "=");
        if (parts.length === 2) return parts.pop().split(";").shift();
        return null;
    }

    // 设置Cookie,minutes为过期分钟数(-1表示删除)
    function blazorSetCookie(name, value, minutes) {
        let expires = "";
        if (minutes > 0) {
            let date = new Date();
            date.setTime(date.getTime() + (minutes * 60 * 1000));
            expires = "; expires=" + date.toUTCString();
        }
        document.cookie = `${name}=${value || ""}${expires}; path=/; secure; sameSite=Strict`;
    }
</script>

3. 两端分别注册服务

  • 服务端项目(Program.cs):
// 注册IHttpContextAccessor
builder.Services.AddHttpContextAccessor();
// 注入IHttpContextAccessor实例到AuthenticationStateProvider
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>(sp => 
    new CustomAuthenticationStateProvider(sp.GetRequiredService<IHttpContextAccessor>()));
  • 客户端项目(Program.cs):
// 注入IJSRuntime实例到AuthenticationStateProvider
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>(sp => 
    new CustomAuthenticationStateProvider(jsRuntime: sp.GetRequiredService<IJSRuntime>()));

关键注意事项

  • 保持HttpOnly=true:服务端设置Cookie时启用该属性,客户端通过页面内JS读取不受影响,同时能防止XSS攻击窃取Token。
  • 安全配置:生产环境确保Secure=true(仅HTTPS传输)、SameSite=Strict(防止CSRF攻击)。
  • 环境判断:通过IHttpContextAccessor是否存在判断运行环境,无需额外依赖环境检测包。

内容的提问来源于stack exchange,提问作者Dhawooth Ibnu Shahaman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 09:15:26