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

Blazor Server中受保护本地存储存储JWT因预渲染问题返回空值,引发API调用401错误

Blazor Server中受保护本地存储存储JWT因预渲染问题返回空值,引发API调用401错误

我理解你现在在开发Blazor Server员工管理系统时遇到的困境:用PostgreSQL存数据、JWT做认证,登录后能成功生成Token,但把Token存在ProtectedLocalStorage后,调用带[Authorize]的API时却拿不到Token,触发了预渲染阶段的JS互操作错误,最终导致请求返回401。咱们先拆解问题根源,再给出具体的解决思路。

问题根源

Blazor Server的预渲染阶段是服务器端生成静态HTML的过程,此时组件还未与浏览器建立JavaScript连接,而ProtectedLocalStorage完全依赖JS互操作访问浏览器本地存储,所以在这个阶段调用它的方法会直接抛出InvalidOperationException。你虽然在App.razor设置了prerender: false,但可能部分页面/组件的预渲染未被彻底禁用,或者在组件交互前(比如AuthStateProvider初始化时)就触发了Token获取操作,导致失败。

可行的解决方案

方案1:TokenProvider兼容预渲染,结合服务器Session存储Token

既然预渲染阶段无法访问浏览器本地存储,我们可以把Token同时存在服务器端Session中,预渲染时从Session读取,客户端渲染完成后再从本地存储读取,实现双存储兼容。

  1. 先在Program.cs中添加Session服务和中间件:
// 注册Session服务
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

// 在路由中间件前启用Session
app.UseSession();
app.UseRouting();
// ...其他中间件
  1. 修改TokenProvider,注入IHttpContextAccessor并兼容Session读取:
public class TokenProvider
{
    public const string TokenKey = "authToken";
    public const string RefreshTokenKey = "refreshToken";
    private readonly ProtectedLocalStorage _storage;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TokenProvider(ProtectedLocalStorage storage, IHttpContextAccessor httpContextAccessor)
    {
        _storage = storage;
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task SaveTokenAsync(string token, string refreshToken)
    {
        await _storage.SetAsync(TokenKey, token);
        await _storage.SetAsync(RefreshTokenKey, refreshToken);
        // 同步保存到服务器Session
        var session = _httpContextAccessor.HttpContext?.Session;
        session?.SetString(TokenKey, token);
        session?.SetString(RefreshTokenKey, refreshToken);
    }

    public async Task<string?> GetTokenAsync()
    {
        try
        {
            var result = await _storage.GetAsync<string>(TokenKey);
            return result.Success ? result.Value : null;
        }
        catch(InvalidOperationException)
        {
            // 预渲染阶段,从Session读取Token
            var session = _httpContextAccessor.HttpContext?.Session;
            return session?.GetString(TokenKey);
        }
        catch(Exception ex)
        {
            Console.WriteLine($"GetTokenAsync failed: {ex.GetType().Name} - {ex.Message}");
            return null;
        }
    }

    public async Task<string?> GetRefreshTokenAsync() 
    {
        try
        {
            var result = await _storage.GetAsync<string>(RefreshTokenKey);
            return result.Success ? result.Value : null;
        }
        catch(InvalidOperationException)
        {
            var session = _httpContextAccessor.HttpContext?.Session;
            return session?.GetString(RefreshTokenKey);
        }
        catch(Exception ex)
        {
            Console.WriteLine($"GetRefreshTokenAsync failed: {ex.GetType().Name} - {ex.Message}");
            return null;
        }
    }

    public async Task RemoveTokenAsync()
    {
        try
        {
            await _storage.DeleteAsync(TokenKey);
            await _storage.DeleteAsync(RefreshTokenKey);
            var session = _httpContextAccessor.HttpContext?.Session;
            session?.Remove(TokenKey);
            session?.Remove(RefreshTokenKey);
        }
        catch
        {
            // 自定义异常处理逻辑
        }
    }
}
  1. 别忘了在Program.cs中注册IHttpContextAccessor:
builder.Services.AddHttpContextAccessor();

方案2:针对特定页面禁用预渲染

如果全局禁用预渲染会影响性能,你可以给需要授权的页面(比如Home页、Logout所在页面)单独设置禁用预渲染:

在Home.razor页面顶部添加:

@page "/"
@rendermode InteractiveServerRenderMode(prerender: false)

这样该页面会直接以交互式模式渲染,跳过预渲染阶段,自然不会触发JS互操作错误。

方案3:修改AuthStateProvider,跳过预渲染阶段的Token获取

AuthStateProvider通常会在页面初始化时被调用,预渲染阶段它尝试获取Token会失败,我们可以在捕获到预渲染错误时返回匿名用户,等客户端渲染完成后再重新获取Token:

public class AuthStateProvider : AuthenticationStateProvider
{
    private readonly TokenProvider _tokenProvider;

    public AuthStateProvider(TokenProvider tokenProvider)
    {
        _tokenProvider = tokenProvider;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        try
        {
            var token = await _tokenProvider.GetTokenAsync();
            if (string.IsNullOrEmpty(token))
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }
            // 替换成你自己的JWT解析逻辑
            var claims = ParseClaimsFromToken(token);
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")));
        }
        catch (InvalidOperationException)
        {
            // 预渲染阶段返回匿名用户,客户端连接建立后会重新触发获取
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }
    }

    // 登录/登出时调用,通知认证状态变更
    public async Task NotifyUserAuthenticated()
    {
        var token = await _tokenProvider.GetTokenAsync();
        var claims = ParseClaimsFromToken(token);
        var identity = new ClaimsIdentity(claims, "jwt");
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity))));
    }

    private IEnumerable<Claim> ParseClaimsFromToken(string token)
    {
        var handler = new JwtSecurityTokenHandler();
        var jwtToken = handler.ReadJwtToken(token);
        return jwtToken.Claims;
    }
}

预渲染时返回匿名用户,等客户端连接建立后,AuthStateProvider会再次调用GetAuthenticationStateAsync,此时就能正常从ProtectedLocalStorage获取Token了。

方案4:确保API调用在组件渲染完成后执行

如果Logout按钮的API调用是在组件初始化时触发的(哪怕是间接触发),要把它移到OnAfterRenderAsync方法中——这个生命周期方法是在组件完成渲染、JS互操作可用后才会执行:

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 在这里执行需要Token的API调用,比如加载用户信息
            // 不要在OnInitializedAsync中执行
        }
        await base.OnAfterRenderAsync(firstRender);
    }
}

总结

优先推荐方案1或方案3,它们能更好地兼容预渲染和交互式渲染场景,同时保证Token的可用性;如果你的项目对预渲染需求不高,方案2是最简单直接的选择。

备注:内容来源于stack exchange,提问作者Prashil Shah

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 18:35:26