Blazor应用集成JWT认证的核心,是让后端API负责验证用户身份并签发令牌,让前端在登录成功后保存令牌,并在后续请求中携带令牌,同时让Blazor组件体系能够感知当前用户是否已经登录。完成这一流程后,页面可以根据认证状态控制导航菜单、路由访问和按钮显示,接口也可以基于Authorization请求头判断请求是否合法。

从职责划分来看,后端API只关心用户身份是否可信,并依据密钥生成带有声明和有效期的令牌;Blazor前端则负责在登录成功后保存令牌,并在访问受保护接口时主动携带令牌;组件系统还需要知道当前访问者是谁,从而决定页面内容如何呈现。因此,集成过程不是简单地把令牌塞进请求头,而是要把签发、保存、携带、解析、状态通知和路由保护串联起来。
一、后端API负责JWT验证与签发
JWT通常由头部、负载和签名三部分组成。头部描述令牌类型和签名算法,负载携带用户声明,例如用户名、角色、过期时间等,签名则用于防止令牌被篡改。后端API在用户登录成功后,会根据用户信息生成JWT并返回给前端;之后前端每次请求受保护接口时,只需要在请求头中携带这个JWT,后端即可通过签名和有效期判断请求是否可信。
在ASP.NET Core中,JWT认证通常通过Bearer认证中间件完成。开发者需要在服务配置中注册认证方案,并设置令牌验证参数,例如签名密钥、发行者、受众、是否验证生命周期等。中间件管道中必须先启用认证,再启用授权,否则即使请求头中包含令牌,也无法正确进入受保护的控制器或接口。
登录接口本身一般允许匿名访问。它接收用户名和密码,验证通过后读取JWT配置,生成包含用户声明的令牌并返回。为了便于维护,密钥、发行者、受众等参数通常放在配置文件中,而不是硬编码在控制器里。下面的示例展示了如何在ASP.NET Core API中完成JWT认证配置、配置文件设置以及登录接口实现。
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer dotnet add package System.IdentityModel.Tokens.Jwt
在项目入口文件中配置JWT认证服务时,需要指定默认的认证方案为JWT Bearer,并通过令牌验证参数告诉框架如何校验传入的令牌。下面的代码展示了最小化托管模型下的常见写法。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// 从配置中读取JWT参数
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var keyBytes = Encoding.ASCII.GetBytes(jwtSettings["Key"] ?? string.Empty);
builder.Services.AddControllers();
// 注册JWT认证
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
var app = builder.Build();
// 认证中间件必须在授权中间件之前执行
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
配置文件用于保存JWT签发和验证所需的基础参数。密钥应具备足够长度,并避免提交到公开代码仓库。发行者和受众可以根据项目环境进行区分,以便后端准确识别令牌来源和使用方。
{
"JwtSettings": {
"Key": "BlazorJwtDemoSecretKey0123456789",
"Issuer": "BlazorJwtIssuer",
"Audience": "BlazorJwtClient"
}
}
登录接口负责验证用户凭证,并在验证通过后生成JWT。示例中使用固定账号仅用于演示,真实项目通常会查询数据库、校验密码哈希,并根据用户表中的角色信息写入不同的声明。
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _configuration;
public AuthController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpPost("login")]
[AllowAnonymous]
public IActionResult Login([FromBody] LoginRequest request)
{
// 示例仅使用固定账号,实际项目应查询数据库并校验密码
if (request.Username == "admin" && request.Password == "123456")
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var keyBytes = Encoding.ASCII.GetBytes(jwtSettings["Key"] ?? string.Empty);
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Role, "Admin")
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(2),
Issuer = jwtSettings["Issuer"],
Audience = jwtSettings["Audience"],
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(keyBytes),
SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return Ok(new { Token = tokenHandler.WriteToken(token) });
}
return Unauthorized("用户名或密码错误");
}
}
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
二、Blazor前端保存Token并自动携带
Blazor前端拿到后端返回的JWT后,首先要解决存储问题。常见做法是将令牌保存到浏览器存储中,例如localStorage或sessionStorage。localStorage适合需要跨会话保持登录状态的场景,sessionStorage则更适合关闭浏览器后就失效的场景。由于Blazor运行在浏览器环境中,访问这些存储通常需要通过JavaScript互操作完成。
将令牌保存到本地只是第一步,更重要的是让后续的HTTP请求自动携带令牌。如果每次调用接口都手动设置请求头,代码会非常分散,也容易遗漏。更合理的方式是创建一个自定义的HTTP消息处理器,在请求发出前统一读取本地令牌,并将其写入Authorization请求头。这样,业务页面只需要注入HttpClient并调用接口,无需重复处理认证头。
下面的TokenService封装了令牌的保存、读取和删除操作。它通过IJSRuntime调用浏览器存储接口,使Blazor组件不需要直接编写JavaScript代码。
using Microsoft.JSInterop;
public class TokenService
{
private const string TokenKey = "jwt_token";
private readonly IJSRuntime _jsRuntime;
public TokenService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task SaveTokenAsync(string token)
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", TokenKey, token);
}
public async Task<string?> GetTokenAsync()
{
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", TokenKey);
}
public async Task RemoveTokenAsync()
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", TokenKey);
}
}
AuthHeaderHandler继承自DelegatingHandler,它会在HttpClient发送请求前执行。只要本地存在令牌,就会将其以Bearer格式加入请求头。这种方式可以集中管理认证信息,也方便后续扩展刷新令牌、统一处理401响应等逻辑。
using System.Net.Http.Headers;
public class AuthHeaderHandler : DelegatingHandler
{
private readonly TokenService _tokenService;
public AuthHeaderHandler(TokenService tokenService)
{
_tokenService = tokenService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await _tokenService.GetTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
}
需要注意的是,浏览器存储虽然使用方便,但并不能完全避免安全风险。如果页面存在脚本注入漏洞,存储在本地的令牌可能被恶意脚本读取。因此,在生产环境中应尽量减少在JWT负载中存放敏感信息,同时确保页面输入输出经过安全处理,并优先使用HTTPS传输。
三、AuthenticationStateProvider同步登录状态
仅有令牌存储和请求携带,Blazor组件仍然无法直接知道当前用户是否已经登录。Blazor的权限组件和认证状态机制依赖AuthenticationStateProvider提供的用户身份。默认情况下,框架并不知道JWT的存在,因此需要自定义一个认证状态提供器,把本地令牌转换成Blazor能够理解的ClaimsPrincipal。
自定义认证状态提供器时,核心方法是GetAuthenticationStateAsync。页面初始化或认证状态发生变化时,Blazor会调用该方法获取当前用户身份。如果本地没有令牌,就返回匿名身份;如果本地存在令牌,就可以根据令牌内容构造已认证身份。登录成功后,还需要调用NotifyAuthenticationStateChanged通知界面刷新,使导航栏、权限区域和受保护页面能够立即响应用户状态变化。
下面的示例为了保持结构清晰,仅根据令牌是否存在构造基础身份。实际项目中,通常会进一步解析JWT中的声明,例如用户名、角色、过期时间等,并在必要时结合后端接口校验令牌是否仍然有效。
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly TokenService _tokenService;
public JwtAuthenticationStateProvider(TokenService tokenService)
{
_tokenService = tokenService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await _tokenService.GetTokenAsync();
if (string.IsNullOrEmpty(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
// 示例仅构造基础身份,实际项目应解析JWT中的Claims并校验有效性
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, "当前用户")
};
var identity = new ClaimsIdentity(claims, "jwt");
var user = new ClaimsPrincipal(identity);
return new AuthenticationState(user);
}
public void NotifyUserAuthentication(string token)
{
// token可在实际项目中进一步解析为Claims
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, "当前用户")
};
var identity = new ClaimsIdentity(claims, "jwt");
var user = new ClaimsPrincipal(identity);
var authState = Task.FromResult(new AuthenticationState(user));
NotifyAuthenticationStateChanged(authState);
}
public void NotifyUserLogout()
{
var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymous));
NotifyAuthenticationStateChanged(authState);
}
}
在Blazor WebAssembly项目中,TokenService、AuthHeaderHandler、JwtAuthenticationStateProvider和HttpClient都可以在项目入口文件中统一注册。将AuthenticationStateProvider注册为JwtAuthenticationStateProvider的同一实例,有助于在页面中直接注入具体类型,从而方便地调用登录和登出通知方法。
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
// 基础服务
builder.Services.AddScoped<TokenService>();
builder.Services.AddScoped<AuthHeaderHandler>();
// 认证状态服务
builder.Services.AddScoped<JwtAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
sp.GetRequiredService<JwtAuthenticationStateProvider>());
builder.Services.AddAuthorizationCore();
// 带JWT请求头的HttpClient
builder.Services.AddScoped(sp =>
{
var authHandler = sp.GetRequiredService<AuthHeaderHandler>();
authHandler.InnerHandler = new HttpClientHandler();
return new HttpClient(authHandler)
{
BaseAddress = new Uri("http://localhost:5000/")
};
});
await builder.Build().RunAsync();
四、登录、登出、路由与元素级权限
登录页面承担的是连接后端接口与前端认证状态的职责。用户提交表单后,前端调用后端登录接口;如果接口返回成功,就把JWT保存到本地,并通知认证状态提供器当前用户已经登录。随后页面跳转到首页或其他目标页面,Blazor组件会根据新的认证状态重新渲染。
登出逻辑则相反。用户退出时,应删除本地保存的令牌,并通知认证状态提供器恢复匿名身份。完成这两步后,受保护路由会重新要求登录,页面上依赖认证状态显示的内容也会随之隐藏。
下面的登录页面示例展示了如何调用后端接口、保存令牌并更新认证状态。这里使用EditForm组织表单,并通过HttpClient提交JSON数据。
@using System.Net.Http.Json
@page "/login"
@inject TokenService TokenService
@inject JwtAuthenticationStateProvider AuthStateProvider
@inject HttpClient Http
@inject NavigationManager Navigation
<h3>用户登录</h3>
<EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
<div class="mb-3">
<label>用户名</label>
<InputText class="form-control" @bind-Value="loginModel.Username" />
</div>
<div class="mb-3">
<label>密码</label>
<InputText type="password" class="form-control" @bind-Value="loginModel.Password" />
</div>
<button type="submit" class="btn btn-primary">登录</button>
</EditForm>
@code {
private LoginRequest loginModel = new();
private async Task HandleLogin()
{
var response = await Http.PostAsJsonAsync("api/auth/login", loginModel);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<LoginResult>();
if (result != null && !string.IsNullOrEmpty(result.Token))
{
await TokenService.SaveTokenAsync(result.Token);
AuthStateProvider.NotifyUserAuthentication(result.Token);
Navigation.NavigateTo("/");
}
}
}
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class LoginResult
{
public string Token { get; set; } = string.Empty;
}
}
登出组件可以放在导航栏或用户菜单中。它不需要再次访问后端登录接口,只需要清理本地令牌并更新Blazor认证状态即可。
@inject TokenService TokenService
@inject JwtAuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation
<button type="button" class="btn btn-link" @onclick="HandleLogout">退出登录</button>
@code {
private async Task HandleLogout()
{
await TokenService.RemoveTokenAsync();
AuthStateProvider.NotifyUserLogout();
Navigation.NavigateTo("/login");
}
}
路由层面的权限控制通常通过根路由中的认证视图完成。当用户访问需要登录的页面时,如果认证状态为未登录,框架会显示未授权提示;如果正在获取认证状态,则可以显示加载提示。这样可以避免受保护页面在身份未确认前被渲染出来。
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>当前内容需要登录后访问。</p>
</NotAuthorized>
<Authorizing>
<p>正在验证身份...</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>未找到请求的页面。</p>
</LayoutView>
</NotFound>
</Router>
如果某个页面只允许特定角色访问,可以在页面顶部使用Authorize特性。下面的示例要求当前用户必须拥有Admin角色,否则无法访问该路由。
@using Microsoft.AspNetCore.Authorization @page "/admin" @attribute [Authorize(Roles = "Admin")] <h3>管理员页面</h3> <p>只有具备管理员角色的用户可以访问该页面。</p>
如果只需要控制页面中的某一块区域,可以使用元素级权限组件。已授权时显示一种内容,未授权时显示另一种内容。这种方式非常适合控制按钮、菜单、操作列或局部提示信息的可见性。
<AuthorizeView Roles="Admin">
<Authorized>
<p>管理员可见内容。</p>
</Authorized>
<NotAuthorized>
<p>当前用户不是管理员,无法查看该内容。</p>
</NotAuthorized>
</AuthorizeView>
在实际项目中,除了完成上述基础流程,还应关注令牌过期后的处理体验。例如,当接口返回未认证状态时,可以清理本地令牌并引导用户重新登录;如果系统需要长时间保持登录,也可以设计刷新令牌机制,在访问令牌即将过期时静默续期。此外,前端不应把用户密码、手机号、身份证号等敏感信息直接写入JWT负载,因为JWT负载只是经过签名保护,并不是加密内容。
总体来看,Blazor集成JWT认证的关键在于三层协作:后端API负责签发和验证令牌,前端HTTP层负责保存和携带令牌,Blazor认证状态层负责把令牌转换为组件可识别的用户身份。只要这三部分职责清晰、注册完整、状态同步及时,就可以在Blazor应用中构建出结构清晰且易于扩展的JWT认证体系。
BlazorJWT认证ASP.NET_Core身份认证Token验证修改时间:2026-07-10 19:18:46