Blazor怎么集成JWT认证

来源:个人站长作者:三上悠亚头衔:网络博主
导读:本期聚焦于三上悠亚创作的《Blazor怎么集成JWT认证》,敬请观看详情。Blazor作为微软推出的前端框架,很多开发者在开发过程中需要实现用户身份认证功能,JWT认证是常用的无状态认证方案。本文会详细介绍Blazor应用集成JWT认证的全流程,包括后端API的JWT生成配置、前端Blazor应用的Token存储与请求携带、认证状态管理以及路由权限控制等核心内容。不管你是刚接触Blazor的新手,还是需要在现有项目中添加认证功能的开发者,都能通过本文的步骤完成JWT认证的集成,实现安全的用户登录与权限管控。

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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。