Skip to content
Go back

ASP.NET Core JWT 认证:从登录到角色授权

JWT authentication 在 ASP.NET Core 里很常见,尤其是前端是 React/Angular、移动端 App,或者 API 需要被多个服务调用时。它的核心思路很直接:用户登录后拿到一个签名 token,后续每次请求都把它放进 Authorization: Bearer <token>,API 验证签名和过期时间,再决定是否放行。

Mukesh Murugan 这篇 .NET 10 教程的价值在于把完整路径串起来了:Identity 负责用户和密码,JsonWebTokenHandler 负责生成 token,AddJwtBearer 负责验证 token,Minimal API endpoint 再用 .RequireAuthorization() 和 role policy 做访问控制。下面按可落地的顺序整理一遍。

先认清 JWT

JWT 是 JSON Web Token 的缩写。它通常长成三段,用点分开:

header.payload.signature

header 描述 token 类型和签名算法,payload 放 claims,比如用户 ID、邮箱、角色、过期时间,signature 则是用服务端密钥对前两段签名后的结果。

这里有个新手很容易忽略的点:payload 只是 Base64 编码,不是加密。任何拿到 token 的人都可以读出 payload 内容。签名只能证明内容没有被篡改,不能隐藏内容。所以 JWT 里不要放密码、密钥、身份证号、银行卡号,甚至不该放任何“泄露后会出事”的敏感信息。

适用场景

JWT 不是所有 ASP.NET Core 应用的默认答案。原文给了一个很实用的取舍:

这篇教程选择 JWT,是因为目标是 Minimal API + 前后端分离风格的 Web API。

准备项目

原文使用 .NET 10 Minimal API。JWT 和 Identity 需要这两个包:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 10.0.0
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 10.0.0

示例为了开箱即跑,使用 EF Core InMemory provider:

dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 10.0.0

InMemory 适合教程和本地演示。真实项目要换成 SQL Server、PostgreSQL 或你正在使用的数据库,用户表和角色表仍然可以由 ASP.NET Core Identity 管理。

用户与 Identity

不要自己写密码哈希。原文用 ASP.NET Core Identity 管用户、密码和角色,并扩展 IdentityUser 增加姓名字段:

public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
}

DbContext 继承 IdentityDbContext<ApplicationUser>,这样 Identity 需要的用户、角色、claim、login 等表结构都会被纳入模型。

public class AppDbContext(DbContextOptions<AppDbContext> options)
    : IdentityDbContext<ApplicationUser>(options)
{
}

Program.cs 里要注册 DbContext 和 Identity。教程用 InMemory database 是为了免掉迁移和数据库连接。

JWT 配置

JWT 配置通常放在 appsettings.json,包括 issuer、audience、signing key 和过期时间。示例可以写在配置文件里,但生产环境不要提交真实 signing key。

{
  "Jwt": {
    "Issuer": "https://localhost:5001",
    "Audience": "https://localhost:5001",
    "Key": "your-long-random-256-bit-secret-key",
    "ExpirationMinutes": 60
  }
}

几个字段要一一对上:

签发 token 和验证 token 时使用的 issuer、audience、key 必须完全一致。很多 401 问题,本质就是这几个值有一个字符不匹配。

生成 Token

.NET 10 中,原文建议使用 JsonWebTokenHandler,而不是更老的 JwtSecurityTokenHandler。生成 token 时通常要放入用户标识、邮箱、名称和角色 claims。

一个 token service 的核心逻辑大致是:

var claims = new List<Claim>
{
    new("id", user.Id),
    new("email", user.Email!),
    new("name", user.UserName!)
};

foreach (var role in roles)
{
    claims.Add(new Claim("role", role));
}

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var descriptor = new SecurityTokenDescriptor
{
    Issuer = jwtOptions.Issuer,
    Audience = jwtOptions.Audience,
    Subject = new ClaimsIdentity(claims),
    Expires = DateTime.UtcNow.AddMinutes(jwtOptions.ExpirationMinutes),
    SigningCredentials = credentials
};

var handler = new JsonWebTokenHandler();
return handler.CreateToken(descriptor);

这里有两个细节值得保留:

一是 role claim 的名字要和后面验证配置保持一致。原文使用 "role",后面也显式配置 RoleClaimType = "role"

二是 signing key 要足够长。HMAC SHA-256 至少需要 256 bit,太短会触发类似 IDX10653 的 key-size 错误,也会让签名安全性变差。

接入 JwtBearer

认证中间件要通过 AddAuthentication().AddJwtBearer(...) 接入。重点是 TokenValidationParameters

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtOptions.Issuer,
            ValidAudience = jwtOptions.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(jwtOptions.Key)),
            NameClaimType = "name",
            RoleClaimType = "role",
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

ClockSkew 默认会给过期时间额外加一点宽限。教程里把它设成 TimeSpan.Zero,这样 token 到点就过期,测试时也更直观。

管道顺序也不能乱:

app.UseAuthentication();
app.UseAuthorization();

认证必须在授权之前。先识别“你是谁”,才能判断“你能不能访问”。

注册与登录

注册接口负责创建用户。它接收邮箱、密码、姓名等字段,调用 UserManager<ApplicationUser>.CreateAsync(...),由 Identity 完成密码哈希和用户持久化。

登录接口负责验证密码。如果邮箱和密码通过验证,就读取用户角色,调用 token service 生成 JWT,然后把 token 返回给客户端。

客户端拿到 token 后,后续请求要带上:

Authorization: Bearer <token>

这一步是 JWT bearer flow 的核心。服务端不需要保存 access token;它只需要用 signing key 验证 token 是否由可信服务签发、是否过期、issuer/audience 是否正确。

保护路由

Minimal API 里保护 endpoint 可以直接链 .RequireAuthorization()

app.MapGet("/profile", (ClaimsPrincipal user) =>
{
    return Results.Ok(new
    {
        Name = user.Identity?.Name,
        Email = user.FindFirstValue("email")
    });
})
.RequireAuthorization();

没有 token 或 token 无效时,返回 401。token 有效但权限不够时,返回 403。

角色授权可以继续加 policy:

app.MapGet("/admin", () => Results.Ok("Admin only"))
    .RequireAuthorization(policy => policy.RequireRole("Admin"));

如果你发现 [Authorize(Roles = "Admin")]RequireRole("Admin") 总是 403,要优先检查 RoleClaimType 是否和 token 里的角色 claim 名称一致。原文把二者都设为 "role",避免 .NET 默认 claim 映射带来的混乱。

怎么测试

最小验证路径可以这样走:

  1. 调用注册接口,创建一个用户。
  2. 调用登录接口,拿到 JWT。
  3. 不带 token 访问受保护 endpoint,应返回 401。
  4. Authorization: Bearer <token> 再访问,应返回 200。
  5. 访问要求 Admin 角色的 endpoint,普通用户应返回 403。
  6. 给用户添加 Admin 角色,重新登录拿新 token,再访问 Admin endpoint,应返回 200。

这里要注意“重新登录”。角色写进 JWT 后,已经签发出去的旧 token 不会自动变化。你给用户加了角色,客户端要拿到新的 token,role claim 才会出现在请求里。

安全边界

原文的安全建议很实际,可以直接当检查清单:

JWT 最大的现实限制是:access token 一旦签发,在过期前通常不能直接撤回。要兼顾安全和体验,需要 refresh token。原文把 refresh token 放到单独文章里讲,这个边界是合理的;不要把 access token 生命周期拉得很长来“省事”。

常见故障

401:先检查请求有没有带 Authorization: Bearer <token>,再检查 issuer、audience、key 是否和签发时一致,最后确认 UseAuthentication()UseAuthorization() 前面。

403:认证通过了,但角色或策略没过。检查 token 里有没有角色 claim,RoleClaimType 是否配置成同一个 claim 名。

IDX10653 或 key-size 错误:signing key 太短。换成足够长的随机密钥。

token 过期时间不符合预期:检查 ClockSkew。默认宽限会让 token 看起来比配置的过期时间更“耐用”。

user.Identity.Name 是空:设置 NameClaimType,并确认 token 中确实有对应 claim。

收个尾

JWT 认证的代码不只是“生成一个字符串”。一套完整实现至少要把这些事对齐:Identity 管用户和密码,token service 生成带 claims 的签名 token,JwtBearer 用同一组 issuer/audience/key 验证,认证中间件排在授权中间件之前,路由用 .RequireAuthorization() 和 role policy 表达访问规则。

真正上线时,还要把 signing key 移出配置文件,缩短 access token 生命周期,补上 refresh token,并明确哪些 claim 可以放进 payload。把这些边界想清楚,JWT 才是可靠的 API 认证方案,而不是一串看起来能用的 bearer token。

如果你关注 AI 助手、开发工具和软件工程实践,可以关注 Aide Hub。这里会继续分享能落地的工具教程、技术观察和项目经验。

参考


Tags


Previous

ASP.NET Core 模型验证:Data Annotations 还是 FluentValidation

Next

ASP.NET Core Middleware:把请求管道顺序讲清楚