基于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
相关产品推荐
相关产品推荐

