如何在Blazor/.NET Core中创建ClaimsPrincipal并同步至WASM与API
解决方案:Blazor WASM + ASP.NET Core 宿主自定义认证适配现有存储
针对你的场景,核心问题在于Blazor WASM是客户端应用,和服务端的认证状态需要通过「持久化身份凭证+客户端状态同步」来打通,你之前的尝试混淆了服务端和客户端的AuthenticationStateProvider职责,下面是具体可落地的方案:
一、核心思路
- 服务端:用Cookie或JWT完成身份持久化,验证密码后生成合法的ClaimsPrincipal并绑定到请求上下文
- 客户端:实现自定义
AuthenticationStateProvider,通过API获取当前用户身份或解析本地凭证,同步UI的认证状态 - 双向打通:API控制器通过
[Authorize]自动识别服务端的ClaimsPrincipal;WASM UI通过自定义认证状态提供者感知用户身份,支持[Authorize]特性和权限控制
二、具体实现步骤
1. 服务端配置(ASP.NET Core 宿主)
1.1 配置Cookie认证(适合同域场景,无需额外存储token)
在Program.cs中添加认证授权服务:
using Microsoft.AspNetCore.Authentication.Cookies; var builder = WebApplication.CreateBuilder(args); // 添加Cookie认证 builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.HttpOnly = true; // 防止XSS options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 生产环境强制HTTPS options.ExpireTimeSpan = TimeSpan.FromHours(8); options.LoginPath = "/api/account/login"; }); builder.Services.AddAuthorization(); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); var app = builder.Build(); // 启用认证授权中间件(顺序不能错) app.UseAuthentication(); app.UseAuthorization(); // 其他中间件配置... app.Run();
1.2 实现登录接口(AccountController)
复用你已有的密码验证逻辑,验证通过后生成ClaimsPrincipal并完成Cookie登录:
[ApiController] [Route("api/account")] public class AccountController : ControllerBase { private readonly IUserStore _userStore; // 你的自定义用户存储服务 public AccountController(IUserStore userStore) { _userStore = userStore; } [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginRequest request) { // 1. 复用旧逻辑验证用户名密码 var user = await _userStore.GetUserByUsername(request.Username); if (user == null || !VerifyPassword(request.Password, user.HashedPassword)) { return Unauthorized("用户名或密码错误"); } // 2. 创建ClaimsPrincipal var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Role, user.Role) // 适配你的权限字段 // 添加其他业务需要的Claims }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); // 3. 写入Cookie,完成服务端登录 await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = request.RememberMe }); // 4. 返回用户基础信息,供客户端同步状态 return Ok(new UserInfoDto { UserId = user.Id.ToString(), Username = user.Username, Role = user.Role }); } // 供客户端获取当前用户身份的接口 [HttpGet("current-user")] [Authorize] public IActionResult GetCurrentUser() { // 从HttpContext.User中读取Claims var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var username = User.FindFirstValue(ClaimTypes.Name); var role = User.FindFirstValue(ClaimTypes.Role); return Ok(new UserInfoDto { UserId = userId, Username = username, Role = role }); } // 密码验证逻辑(复用你的旧代码) private bool VerifyPassword(string inputPassword, string hashedPassword) { // 示例:用BCrypt验证,替换成你的旧哈希验证逻辑 return BCrypt.Net.BCrypt.Verify(inputPassword, hashedPassword); } } // 辅助DTO public class LoginRequest { public string Username { get; set; } public string Password { get; set; } public bool RememberMe { get; set; } } public class UserInfoDto { public string UserId { get; set; } public string Username { get; set; } public string Role { get; set; } }
2. 客户端配置(Blazor WASM)
2.1 实现自定义AuthenticationStateProvider
WASM无法直接读取HttpOnly Cookie,所以需要通过API获取用户身份,同步UI的认证状态:
using System.Security.Claims; using System.Net.Http.Json; public class CustomAuthStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; private readonly ClaimsPrincipal _anonymousUser = new(new ClaimsIdentity()); public CustomAuthStateProvider(HttpClient httpClient) { _httpClient = httpClient; } // 核心方法:返回当前认证状态 public override async Task<AuthenticationState> GetAuthenticationStateAsync() { try { // 调用服务端接口获取当前用户信息 var response = await _httpClient.GetAsync("api/account/current-user"); if (response.IsSuccessStatusCode) { var userInfo = await response.Content.ReadFromJsonAsync<UserInfoDto>(); if (userInfo != null) { // 构建客户端的ClaimsPrincipal var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userInfo.UserId), new(ClaimTypes.Name, userInfo.Username), new(ClaimTypes.Role, userInfo.Role) }; var identity = new ClaimsIdentity(claims, "CookieAuth"); var principal = new ClaimsPrincipal(identity); return new AuthenticationState(principal); } } } catch (Exception) { // 网络错误或未登录时返回匿名用户 } return new AuthenticationState(_anonymousUser); } // 登录成功后手动触发状态更新 public void NotifyAuthStateChanged() { NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }
2.2 注册自定义认证服务
在WASM项目的Program.cs中替换默认的AuthenticationStateProvider:
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<HeadOutlet>("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); // 注册自定义认证状态提供者 builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>(); // 添加授权服务 builder.Services.AddAuthorizationCore(); await builder.Build().RunAsync();
2.3 实现WASM登录组件
登录成功后触发认证状态更新,让UI感知用户已登录:
@page "/login" @inject HttpClient Http @inject AuthenticationStateProvider AuthStateProvider @inject NavigationManager NavManager <h3>登录</h3> <div class="mb-3"> <label>用户名</label> <input @bind="@Username" class="form-control" /> </div> <div class="mb-3"> <label>密码</label> <input type="password" @bind="@Password" class="form-control" /> </div> <div class="mb-3"> <input type="checkbox" @bind="@RememberMe" /> 记住我 </div> <button @onclick="HandleLogin" class="btn btn-primary">登录</button> @if (!string.IsNullOrEmpty(ErrorMessage)) { <div class="text-danger mt-2">@ErrorMessage</div> } @code { private string Username { get; set; } private string Password { get; set; } private bool RememberMe { get; set; } private string ErrorMessage { get; set; } private async Task HandleLogin() { ErrorMessage = null; var request = new LoginRequest { Username = Username, Password = Password, RememberMe = RememberMe }; var response = await Http.PostAsJsonAsync("api/account/login", request); if (response.IsSuccessStatusCode) { // 通知认证状态更新,UI会自动刷新[Authorize]组件 ((CustomAuthStateProvider)AuthStateProvider).NotifyAuthStateChanged(); NavManager.NavigateTo("/"); } else { ErrorMessage = await response.Content.ReadAsStringAsync(); } } }
3. 验证授权效果
- API控制器:在需要授权的控制器/方法上添加
[Authorize]特性,User属性会自动绑定到当前ClaimsPrincipal:[ApiController] [Route("api/admin")] [Authorize(Roles = "Admin")] public class AdminController : ControllerBase { [HttpGet("stats")] public IActionResult GetAdminStats() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); return Ok($"管理员 {userId} 查看统计数据"); } } - WASM UI:使用
[Authorize]特性控制组件显示,或通过AuthenticationStateProvider获取用户身份:<AuthorizeView Roles="Admin"> <Authorized> <p>欢迎管理员 @context.User.Identity.Name</p> <NavLink href="/admin">管理后台</NavLink> </Authorized> <NotAuthorized> <p>您没有管理员权限</p> </NotAuthorized> </AuthorizeView>
三、常见问题说明
- 为什么之前的方法无效?
- 服务端的
AuthenticationStateProvider只对Blazor Server有效,WASM的认证状态由客户端的提供者管理 SignInManager是ASP.NET Core Identity专属组件,不用Identity的话无法兼容- 直接设置
HttpContext.User仅对当前请求有效,必须通过Cookie/JWT持久化身份
- 服务端的
- 如果想用JWT代替Cookie?
- 服务端配置
JwtBearer认证,登录接口返回JWT token - 客户端将token存储在
localStorage,自定义AuthenticationStateProvider解析token生成ClaimsPrincipal - 每次API请求在Header中携带
Authorization: Bearer {token}
- 服务端配置
内容的提问来源于stack exchange,提问作者Jeff Whitty
相关产品推荐
相关产品推荐

