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

基于Cookie实现MudBlazor/.NET8应用页面刷新后保持登录状态

问题描述

我正在开发一款基于MudBlazor、C# .NET 8的内部工具,用于展示数据库数据。该应用通过CustomAuthStateProvider管理认证,支持用户登录/登出,已实现对认证用户的操作限制功能。

此前遇到使用forceLoad: true导航时应用崩溃的问题,报错InvalidOperationException: 无法找到所需的'IAuthenticationService'服务,已通过修改App.razor的渲染模式、调整JSInterop使用方式,以及实现BlazorAuthorizationMiddlewareResultHandler解决该问题。

当前核心问题:手动输入URL或刷新页面时,用户会被登出,无报错信息。尝试使用Cookie认证但未成功,未使用JWT。目前已不在页面上使用@attribute [Authorize],改为在代码中检查登录状态并提前返回。需要实现Cookie认证以保持登录状态。

相关代码

Program.cs

using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor.Services;

var builder = WebApplication.CreateBuilder(args);

// Add MudBlazor services
builder.Services.AddMudServices();

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddSingleton<InitializationStateService>(); // Logging accesses to json file

builder.Services.AddScoped<CustomAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthStateProvider>());
builder.Services.AddAuthorizationCore();
builder.Services.AddSingleton<Microsoft.AspNetCore.Authorization.IAuthorizationMiddlewareResultHandler, BlazorAuthorizationMiddlewareResultHandler>();

builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "auth_token";
        options.Cookie.MaxAge = TimeSpan.FromMinutes(30);
        options.LoginPath = "/login";
        options.LogoutPath = "/logout";
        options.ExpireTimeSpan = TimeSpan.FromHours(1);
        options.SlidingExpiration = true;
    });

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();

builder.Services.AddServerSideBlazor().AddCircuitOptions(options =>
{
    options.DetailedErrors = true;
    options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(0);
    options.DisconnectedCircuitMaxRetained = 0;
});

builder.Services.AddHealthChecks();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
}

app.UseStaticFiles();
app.UseAntiforgery();

app.UseAuthentication();
app.UseAuthorization();

app.UseHealthChecks("/health");

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

CustomAuthStateProvider.cs

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public CustomAuthStateProvider()
    {
        this.CurrentUser = this.GetAnonymous();
    }

    private ClaimsPrincipal CurrentUser { get; set; }

    private ClaimsPrincipal GetUser(string userName, uint id, string role)
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Sid, id.ToString(new System.Globalization.CultureInfo("de-DE"))),
            new Claim(ClaimTypes.Name, userName),
            new Claim(ClaimTypes.Role, role)
        }, CookieAuthenticationDefaults.AuthenticationScheme);
        return new ClaimsPrincipal(identity);
    }

    private ClaimsPrincipal GetAnonymous()
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Sid, "0"),
            new Claim(ClaimTypes.Name, "Anonymous"),
            new Claim(ClaimTypes.Role, "Anonymous")
        }, null);
        return new ClaimsPrincipal(identity);
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var task = Task.FromResult(new AuthenticationState(this.CurrentUser));
        return task;
    }

    public Task<AuthenticationState> ChangeUser(string username, uint id, string role)
    {
        this.CurrentUser = this.GetUser(username, id, role);
        var task = this.GetAuthenticationStateAsync();
        this.NotifyAuthenticationStateChanged(task);
        return task;
    }

    public Task<AuthenticationState> Logout()
    {
        this.CurrentUser = this.GetAnonymous();
        var task = this.GetAuthenticationStateAsync();
        this.NotifyAuthenticationStateChanged(task);
        return task;
    }
}

BlazorAuthorizationMiddlewareResultHandler.cs

public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        return next(context);
    }
}

Routes.razor

<Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView
 RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
解决方案

你的核心问题是CustomAuthStateProvider仅依赖内存维护用户状态,刷新或手动输入URL时内存重置,导致用户变回匿名。以下是针对性修改步骤:

1. 改造CustomAuthStateProvider,从Cookie读取认证信息

注入IHttpContextAccessor,让Provider能访问请求Cookie并同步认证状态:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System.Threading.Tasks;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private ClaimsPrincipal _currentUser;

    public CustomAuthStateProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
        _currentUser = GetCurrentUserFromCookie();
    }

    private ClaimsPrincipal GetCurrentUserFromCookie()
    {
        var httpContext = _httpContextAccessor.HttpContext;
        // 从HttpContext读取已认证的用户信息
        if (httpContext?.User?.Identity?.IsAuthenticated ?? false)
        {
            return httpContext.User;
        }
        return GetAnonymous();
    }

    private ClaimsPrincipal GetUser(string userName, uint id, string role)
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Sid, id.ToString()),
            new Claim(ClaimTypes.Name, userName),
            new Claim(ClaimTypes.Role, role)
        }, CookieAuthenticationDefaults.AuthenticationScheme);
        return new ClaimsPrincipal(identity);
    }

    private ClaimsPrincipal GetAnonymous()
    {
        var identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Sid, "0"),
            new Claim(ClaimTypes.Name, "Anonymous"),
            new Claim(ClaimTypes.Role, "Anonymous")
        }, null);
        return new ClaimsPrincipal(identity);
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        return Task.FromResult(new AuthenticationState(_currentUser));
    }

    public async Task<AuthenticationState> ChangeUser(string username, uint id, string role)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Sid, id.ToString()),
            new Claim(ClaimTypes.Name, username),
            new Claim(ClaimTypes.Role, role)
        };

        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        _currentUser = new ClaimsPrincipal(identity);

        // 将用户信息写入Cookie,持久化认证状态
        var authProperties = new AuthenticationProperties
        {
            ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1),
            IsPersistent = true,
            AllowRefresh = true
        };

        await _httpContextAccessor.HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            _currentUser,
            authProperties);

        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        return await GetAuthenticationStateAsync();
    }

    public async Task<AuthenticationState> Logout()
    {
        _currentUser = GetAnonymous();
        // 清除认证Cookie
        await _httpContextAccessor.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        return await GetAuthenticationStateAsync();
    }
}

2. 在Program.cs中注册IHttpContextAccessor

在注册CustomAuthStateProvider前添加:

builder.Services.AddHttpContextAccessor();

3. 调整登录逻辑,确保写入Cookie

登录组件中调用ChangeUser方法完成认证,例如:

@inject CustomAuthStateProvider AuthProvider
@inject NavigationManager NavigationManager

// 登录按钮点击事件
private async Task HandleLogin()
{
    // 此处替换为实际的用户名密码验证逻辑
    await AuthProvider.ChangeUser("testuser", 1, "Admin");
    NavigationManager.NavigateTo("/", forceLoad: false);
}

4. 可选:恢复路由级授权(Routes.razor)

如果需要重新启用路由级权限控制,修改Routes.razor:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
                <NotAuthorized>
                    <p>无权限访问此页面,请登录</p>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(Layout.MainLayout)">
                <p>抱歉,该页面不存在</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

关键说明

  • 改造后的CustomAuthStateProvider会在初始化时从HttpContext读取Cookie中的认证信息,解决刷新/手动输入URL时的状态丢失问题。
  • ChangeUser方法通过SignInAsync将用户信息写入Cookie,实现跨请求的状态持久化。
  • Logout方法通过SignOutAsync清除认证Cookie,完成登出操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 15:07:10