一、JWT认证流程全景JSON Web TokenJWT是现代Web应用最广泛采用的无状态认证方案。与传统的基于Session的认证不同服务器不需要在内存或数据库中存储会话只需颁发一个包含用户身份声明的加密令牌客户端在每次请求时携带该令牌服务器验证令牌签名的有效性即可判断用户身份。这种设计天然适合前后端分离架构和水平扩展场景。JWT令牌由三部分组成用.分隔Header算法声明、Payload声明集合如用户ID、角色、过期时间和Signature使用服务器密钥对前两部分的HMAC签名。任何人可以解码Payload看到声明内容但无法在不知道密钥的情况下伪造有效签名这保证了令牌的完整性。完整的JWT认证流程如下用户 → 输入凭据 → Blazor前端 → POST /api/auth/login → API服务器 ↓ 验证凭据 颁发 AccessToken RefreshToken API服务器 → 返回令牌 → Blazor前端存储令牌 → 后续请求携带 Authorization: Bearer token ↓ 验证签名 API服务器 → 返回受保护数据 → Blazor前端展示二、API服务器端JWT配置首先在API项目安装所需包dotnetaddpackage Microsoft.AspNetCore.Authentication.JwtBearer dotnetaddpackage System.IdentityModel.Tokens.Jwt在appsettings.json中存放JWT配置生产环境的密钥应通过环境变量或密钥管理服务注入绝对不要将真实密钥提交到代码仓库{Jwt:{Issuer:https://api.myapp.com,Audience:https://myapp.com,SecretKey:your-256-bit-secret-key-here-replace-in-production,AccessTokenExpirationMinutes:60,RefreshTokenExpirationDays:30}}在Program.cs配置JWT Bearer认证// Program.csAPI项目varjwtConfigbuilder.Configuration.GetSection(Jwt);varsecretKeyjwtConfig[SecretKey]!;builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options{options.TokenValidationParametersnewTokenValidationParameters{// 验证令牌的签发者服务器声明自己是谁ValidateIssuertrue,ValidIssuerjwtConfig[Issuer],// 验证令牌的受众令牌是为哪个应用签发的ValidateAudiencetrue,ValidAudiencejwtConfig[Audience],// 验证令牌是否已过期必须开启ValidateLifetimetrue,// 验证签名密钥核心安全性ValidateIssuerSigningKeytrue,IssuerSigningKeynewSymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),// 令牌过期的时钟偏差容忍默认5分钟建议设为0ClockSkewTimeSpan.Zero};});builder.Services.AddAuthorization();// 中间件顺序Authentication 必须在 Authorization 之前app.UseAuthentication();app.UseAuthorization();登录端点负责验证用户凭据并签发令牌// 登录请求和响应 DTOpublicrecordLoginRequest(stringEmail,stringPassword);publicrecordLoginResponse(stringAccessToken,stringRefreshToken,DateTimeExpiresAt);// POST /api/auth/loginapp.MapPost(/api/auth/login,async(LoginRequestrequest,IUserServiceuserService,IConfigurationconfig){// 验证用户凭据具体实现依赖用户服务和密码哈希策略varuserawaituserService.ValidateCredentialsAsync(request.Email,request.Password);if(userisnull)returnResults.Unauthorized();// 构建JWT声明集合ClaimsvarclaimsnewListClaim{// subSubject唯一标识用户身份new(JwtRegisteredClaimNames.Sub,user.Id.ToString()),new(JwtRegisteredClaimNames.Email,user.Email),new(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),// 唯一标识此令牌// 用户角色支持多角色new(ClaimTypes.Role,user.Role),new(ClaimTypes.Name,user.DisplayName)};varjwtSectionconfig.GetSection(Jwt);varkeynewSymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection[SecretKey]!));varexpiresAtDateTime.UtcNow.AddMinutes(jwtSection.GetValueint(AccessTokenExpirationMinutes));vartokennewJwtSecurityToken(issuer:jwtSection[Issuer],audience:jwtSection[Audience],claims:claims,expires:expiresAt,signingCredentials:newSigningCredentials(key,SecurityAlgorithms.HmacSha256));varaccessTokennewJwtSecurityTokenHandler().WriteToken(token);// RefreshToken 是不透明的随机字符串存储于数据库供刷新使用varrefreshTokenawaituserService.GenerateRefreshTokenAsync(user.Id);returnResults.Ok(newLoginResponse(accessToken,refreshToken,expiresAt));});// 需要认证的端点加 .RequireAuthorization() 过滤器app.MapGet(/api/users/me,(ClaimsPrincipaluser){// User.FindFirst 从JWT Claims中读取用户信息returnResults.Ok(new{Iduser.FindFirst(JwtRegisteredClaimNames.Sub)?.Value,Emailuser.FindFirst(JwtRegisteredClaimNames.Email)?.Value,Nameuser.FindFirst(ClaimTypes.Name)?.Value,Roleuser.FindFirst(ClaimTypes.Role)?.Value});}).RequireAuthorization();// 未携带有效JWT将返回401三、Blazor前端自定义AuthenticationStateProvider在Blazor的认证体系中AuthenticationStateProvider是一个抽象基类负责告知框架当前用户是谁。AuthorizeView组件和[Authorize]特性都依赖它来判断认证和授权状态。对于JWT认证方案我们需要自定义这个提供者让它从本地存储中读取JWT令牌并解析用户身份。// Auth/JwtAuthenticationStateProvider.csusingSystem.IdentityModel.Tokens.Jwt;usingSystem.Security.Claims;usingMicrosoft.AspNetCore.Components.Authorization;publicclassJwtAuthenticationStateProvider:AuthenticationStateProvider{privatereadonlyLocalStorageService_localStorage;privatereadonlyHttpClient_http;// 表示未认证的匿名用户状态静态复用避免重复创建privatestaticreadonlyAuthenticationStateAnonymousStatenew(newClaimsPrincipal(newClaimsIdentity()));publicJwtAuthenticationStateProvider(LocalStorageServicelocalStorage,HttpClienthttp){_localStoragelocalStorage;_httphttp;}// 框架在需要认证状态时调用此方法publicoverrideasyncTaskAuthenticationStateGetAuthenticationStateAsync(){vartokenawait_localStorage.GetAsyncstring(accessToken);if(string.IsNullOrWhiteSpace(token))returnAnonymousState;// 解析JWT令牌中的Claims无需请求服务器varhandlernewJwtSecurityTokenHandler();JwtSecurityTokenjwtToken;try{jwtTokenhandler.ReadJwtToken(token);}catch{// Token格式错误视为未认证returnAnonymousState;}// 检查令牌是否已过期if(jwtToken.ValidToDateTime.UtcNow){// 令牌已过期清除本地存储awaitClearTokenAsync();returnAnonymousState;}// 从JWT Payload中提取Claims构建已认证的ClaimsPrincipal// 必须指定 authenticationType 才会被识别为已认证身份IsAuthenticated truevaridentitynewClaimsIdentity(jwtToken.Claims,jwt);varusernewClaimsPrincipal(identity);returnnewAuthenticationState(user);}// 登录成功后调用保存令牌并通知框架认证状态已变化publicasyncTaskLoginAsync(stringaccessToken,stringrefreshToken){await_localStorage.SetAsync(accessToken,accessToken);await_localStorage.SetAsync(refreshToken,refreshToken);// 将JWT添加到所有后续HTTP请求的Authorization头_http.DefaultRequestHeaders.AuthorizationnewSystem.Net.Http.Headers.AuthenticationHeaderValue(Bearer,accessToken);// 重新解析认证状态并通知订阅者触发授权相关组件重渲染varstateawaitGetAuthenticationStateAsync();NotifyAuthenticationStateChanged(Task.FromResult(state));}// 退出登录清除令牌并通知框架publicasyncTaskLogoutAsync(){awaitClearTokenAsync();_http.DefaultRequestHeaders.Authorizationnull;NotifyAuthenticationStateChanged(Task.FromResult(AnonymousState));}// 应用启动时恢复认证状态从localStorage读取并设置HTTP头publicasyncTaskInitializeAsync(){vartokenawait_localStorage.GetAsyncstring(accessToken);if(!string.IsNullOrEmpty(token)){_http.DefaultRequestHeaders.AuthorizationnewSystem.Net.Http.Headers.AuthenticationHeaderValue(Bearer,token);}}privateasyncTaskClearTokenAsync(){await_localStorage.RemoveAsync(accessToken);await_localStorage.RemoveAsync(refreshToken);}}在Program.cs中注册认证相关服务// Blazor WASM 前端 Program.csbuilder.Services.AddScopedLocalStorageService();builder.Services.AddScopedJwtAuthenticationStateProvider();// 将自定义Provider注册为 AuthenticationStateProvider供框架使用builder.Services.AddScopedAuthenticationStateProvider(spsp.GetRequiredServiceJwtAuthenticationStateProvider());// 添加级联认证状态让 AuthorizeView 等组件能收到认证状态推送builder.Services.AddAuthorizationCore();登录页面组件*Components/Pages/Login.razor* page/logininject JwtAuthenticationStateProvider AuthProvider inject NavigationManager NavManager inject ProductApiClient ProductApih1登录/h1EditFormModelloginModelOnValidSubmitHandleLoginDataAnnotationsValidator/ValidationSummary/divclassmb-3label邮箱/labelInputText bind-ValueloginModel.Emailclassform-control//divdivclassmb-3label密码/labelInputTexttypepasswordbind-ValueloginModel.Passwordclassform-control//divif(!string.IsNullOrEmpty(errorMessage)){divclassalert alert-dangererrorMessage/div}buttontypesubmitclassbtn btn-primarydisabledisLoading(isLoading?登录中...:登录)/button/EditFormcode{[SupplyParameterFromQuery]privatestringReturnUrl{get;set;}/;privateLoginModelloginModelnew();privatestring?errorMessage;privateboolisLoadingfalse;privateasyncTaskHandleLogin(){isLoadingtrue;errorMessagenull;try{varresponseawaitProductApi.LoginAsync(newLoginRequest(loginModel.Email,loginModel.Password));// 保存令牌并更新认证状态awaitAuthProvider.LoginAsync(response.AccessToken,response.RefreshToken);// 登录成功后跳转到原目标页面NavManager.NavigateTo(ReturnUrl);}catch(HttpRequestExceptionex)when(ex.StatusCodeSystem.Net.HttpStatusCode.Unauthorized){errorMessage邮箱或密码错误请重试。;}finally{isLoadingfalse;}}privateclassLoginModel{[Required]publicstringEmail{get;set;}string.Empty;[Required]publicstringPassword{get;set;}string.Empty;}}四、策略授权与角色授权简单的角色授权[Authorize(Roles Admin)]足以应付大多数场景但对于复杂的权限规则ASP.NET Core提供了策略授权Policy-based authorization允许用C#代码定义任意复杂的授权逻辑。在API端定义授权策略builder.Services.AddAuthorization(options{// 策略必须是高级 VIP 用户角色为 VIP 且账号年龄超过30天options.AddPolicy(SeniorVip,policypolicy.RequireRole(VIP).RequireAssertion(context{varregisteredAtClaimcontext.User.FindFirst(registered_at)?.Value;if(DateTime.TryParse(registeredAtClaim,outvarregisteredAt))return(DateTime.UtcNow-registeredAt).TotalDays30;returnfalse;}));// 策略需要特定权限声明options.AddPolicy(CanManageProducts,policypolicy.RequireClaim(permission,products:write));});在Blazor前端AuthorizeView组件可以接收Policy或Roles参数做细粒度UI控制*基于策略控制UI可见性*AuthorizeViewPolicyCanManageProductsAuthorizedbuttonclassbtn btn-primaryonclickCreateProduct新增产品/button/Authorized/AuthorizeView*同时订阅认证状态获取当前用户信息*AuthorizeViewAuthorizedContextauthStatep欢迎authState.User.FindFirst(ClaimTypes.Name)?.Value/pbuttononclickLogout退出登录/button/AuthorizedNotAuthorizedahref/login请先登录/a/NotAuthorized/AuthorizeView五、总结本章完整实现了JWT认证的全链路API端用AddJwtBearer配置令牌验证参数登录端点签发包含用户声明的访问令牌Blazor前端通过自定义JwtAuthenticationStateProvider从本地存储恢复认证状态并在AuthorizeView和[Authorize]组件的支持下实现声明式UI权限控制基于声明和断言的策略授权则提供了超越简单角色判断的灵活授权能力。但是并非所有的信息交换都适合请求-响应模式。当服务器需要主动向客户端推送数据时——例如实时聊天消息、股票行情更新、协作文档的他人编辑通知——轮询API会造成大量浪费。下一章我们将介绍实时通信的利器SignalR探讨如何在Blazor应用中构建真正的双向实时通道。