Blazor .NET8 Server Interactive持久化认证实现问题求助
Blazor .NET8 Server Interactive 持久化认证实现方案
问题描述
我是一名WinForm/WPF开发者,刚接触Web开发,目前在Blazor .NET8 Server Interactive应用中无法实现可持久化的认证系统。登录登出功能可正常工作,网站内导航时用户状态也能保留,但刷新页面(F5)或打开新标签页后,用户就会失去登录状态。我明白这是因为用户状态存储在AuthenticationManager的私有字段中,现有的自定义AuthenticationStateProvider代码如下:
public class AuthenticationManager(ILogger<AuthenticationManager> logger) : AuthenticationStateProvider { private ClaimsPrincipal _user = new(); public override Task<AuthenticationState> GetAuthenticationStateAsync() { return Task.FromResult(new AuthenticationState(_user)); } public ClaimsPrincipal? LogInUser(string decryptedName, string decryptedPassword) { try { // 检查是否提供了用户名和密码 if (!string.IsNullOrEmpty(decryptedName) && !string.IsNullOrEmpty(decryptedPassword)) { // 在数据库中查找对应用户 using DBInterface dbManager = new(); TUser? dbUser = dbManager.GetAllUsers().FirstOrDefault(u => string.Compare(decryptedName, u.Name, true) == 0); // 验证密码 if (dbUser != null && Simple3Des.DecryptString(dbUser.PasswordHash) == decryptedPassword) { List<Claim> claims = [ new(ClaimTypes.Name, decryptedName), new(ClaimTypes.Role, dbUser.AccessLevelEnu.ToString()) ]; ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme); _user = new(claimsIdentity); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); return _user; } } } catch (Exception ex) { logger!.LogError("Erreur dans {methodName} : {exceptionType} : {exceptionMessage}", $"{nameof(AuthenticationManager)}.{nameof(LogInUser)}", ex.GetType().Name, ex.Message); throw; } return null; } public void LogOutUser() { _user = new(); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }
解决方案
要实现持久化认证,核心是把用户的认证状态存储到Cookie中(Web应用的标准持久化方式),而不是仅存在服务端内存里。以下是具体实现步骤:
1. 配置Cookie认证服务
在Program.cs中添加Cookie认证的配置:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/Login"; // 未认证时跳转的登录页路径 options.Cookie.Name = "BlazorAuthCookie"; // Cookie名称 options.Cookie.HttpOnly = true; // 禁止JS读取Cookie,提升安全性 options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 生产环境建议启用,仅通过HTTPS传输 options.ExpireTimeSpan = TimeSpan.FromDays(7); // Cookie有效期 }); // 必须添加授权服务 builder.Services.AddAuthorization();
同时要确保在中间件管道中启用认证和授权:
app.UseAuthentication(); app.UseAuthorization();
2. 修改自定义AuthenticationManager
注入IHttpContextAccessor来操作Cookie,同时修改GetAuthenticationStateAsync从Cookie读取认证状态,登录时写入Cookie,登出时删除Cookie:
public class AuthenticationManager(ILogger<AuthenticationManager> logger, IHttpContextAccessor httpContextAccessor) : AuthenticationStateProvider { private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) { return new AuthenticationState(new ClaimsPrincipal()); } // 从Cookie中获取认证票据 var authResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); return new AuthenticationState(authResult.Principal ?? new ClaimsPrincipal()); } public async Task<ClaimsPrincipal?> LogInUser(string decryptedName, string decryptedPassword) { try { if (!string.IsNullOrEmpty(decryptedName) && !string.IsNullOrEmpty(decryptedPassword)) { using DBInterface dbManager = new(); TUser? dbUser = dbManager.GetAllUsers().FirstOrDefault(u => string.Compare(decryptedName, u.Name, true) == 0); if (dbUser != null && Simple3Des.DecryptString(dbUser.PasswordHash) == decryptedPassword) { List<Claim> claims = [ new(ClaimTypes.Name, decryptedName), new(ClaimTypes.Role, dbUser.AccessLevelEnu.ToString()) ]; ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); // 创建认证票据并写入Cookie await _httpContextAccessor.HttpContext!.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, new AuthenticationProperties { IsPersistent = true, // 持久化Cookie,关闭浏览器后仍保留 ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7) }); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); return claimsPrincipal; } } } catch (Exception ex) { logger!.LogError("Erreur dans {methodName} : {exceptionType} : {exceptionMessage}", $"{nameof(AuthenticationManager)}.{nameof(LogInUser)}", ex.GetType().Name, ex.Message); throw; } return null; } public async Task LogOutUser() { await _httpContextAccessor.HttpContext!.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }
然后在Program.cs中注册IHttpContextAccessor和自定义AuthenticationStateProvider:
builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped<AuthenticationStateProvider, AuthenticationManager>();
3. 关键注意事项
- 密码存储风险:当前代码中对数据库存储的密码哈希进行解密后验证,这是非常不安全的做法。正确的方式是永远不存储明文密码,也不解密存储的哈希,应该在用户注册时对明文密码进行哈希(比如使用
BCrypt或ASP.NET Core Identity的密码哈希器),验证时直接将输入的明文密码哈希后与数据库中的哈希值比对。 - Blazor Server交互模式:Server Interactive模式下,
IHttpContextAccessor在组件初始化时可正常获取上下文,但后台线程中可能无法访问,不过登录登出操作通常由用户交互触发,不会有问题。 - Cookie安全性:生产环境务必启用
SecurePolicy = CookieSecurePolicy.Always,确保Cookie仅通过HTTPS传输,防止被窃听。
内容的提问来源于stack exchange,提问作者jérémie Courbat
相关产品推荐
相关产品推荐

