文章元信息
- 作者:刚子
- 系列名称:.NET 8 现代Web开发实战指南
- 原文链接:https://www.codeobservatory.cn/post/dotnet-8-web-development-part7-jwt-authentication-authorization
- 关键词:JWT, JSON Web Token, 身份验证, 授权, 接口安全, Bearer Token, .NET 8 Security
- 摘要:本文将深入浅出地讲解 JWT(JSON Web Token)在 .NET 8 中的应用。从原理到代码实战,手把手教你搭建用户登录接口颁发 Token,并配置 API 网关验证 Token,最终实现基于角色的接口权限控制,保护你的 API 不被非法访问。
一、前言:给 API 装个“门禁卡”
在前面的文章里,我们的 Todo API 就像一栋没有大门的房子,任何人都可以随意进出(调用接口)。这在企业级应用中是绝对不允许的。
我们需要一套机制:
- 认证:你是谁?(查验身份证)
- 授权:你能干什么?(查看权限列表)
在前后端分离的架构中,JWT (JSON Web Token) 是目前最流行的认证方案。它就像一张“电子门禁卡”。用户登录成功后,服务器发给他一张卡(Token),以后每次请求都要带着这张卡,服务器只需验证卡的真伪,而不需要每次都去查数据库。
二、JWT 到底是什么?
刚子不喜欢背书,你只需要理解三个核心点:
一个 JWT 就是一个很长的字符串,长得像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuWkp...
它由三部分组成(用 . 分隔):
- Header(头):说明这是什么卡,用什么加密算法。
- Payload(载荷):重点。里面存着用户信息(如用户ID、用户名、角色)。注意:这里的数据只是 Base64 编码,不要存密码!
- Signature(签名):核心安全层。服务器用只有自己知道的密钥,对前两部分进行签名。黑客如果篡改了 Payload,签名就对不上了,服务器会直接拒绝。
【配图建议 1】 位置:JWT 组成部分讲解之后。 内容:一张清晰的 JWT 结构图,标注 Header、Payload、Signature 三部分,并示意 Payload 中包含
name、role等信息。 Alt文本:JWT (JSON Web Token) 的三部分结构示意图。
三、实战准备:安装 NuGet 包
在项目中安装以下 NuGet 包:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
这会引入处理 JWT 验证的核心中间件。
四、第一步:打造“发卡处”
用户登录时,我们需要核实账号密码,然后生成 Token 发给他。为了演示方便,我们假设有一个模拟的用户库。
4.1 定义配置模型和模拟用户
在 Program.cs 顶部定义模型:
// 定义用户模型
public class User
{
public string Username { get; set; }
public string Password { get; set; } // 实际项目中应存储哈希值
public string Role { get; set; } // 角色:Admin, User
}
// 模拟数据库用户
public static class UserStore
{
public static List<User> Users = new List<User>
{
new User { Username = "admin", Password = "123456", Role = "Admin" },
new User { Username = "gangzi", Password = "123456", Role = "User" }
};
}
4.2 编写登录接口
这个接口负责“发卡”。
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
// ... builder 配置 ...
app.MapPost("/login", (User login) =>
{
// 1. 验证账号密码
var user = UserStore.Users.FirstOrDefault(u => u.Username == login.Username && u.Password == login.Password);
if (user == null)
{
return Results.Unauthorized(); // 401 未授权
}
// 2. 创建 Claims(声明) - 也就是 Payload 里要存的数据
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role), // 存入角色,用于授权
new Claim("MyCustomClaim", "SomeData") // 也可以存自定义数据
};
// 3. 生成签名密钥(实际项目中应从 appsettings.json 读取)
// 密钥至少要 16 个字符
var secretKey = "This_Is_A_Very_Secret_Key_For_JWT_2024!";
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 4. 生成 Token
var token = new JwtSecurityToken(
issuer: "MyTodoApp", // 签发者
audience: "MyTodoAppUsers", // 接收者
claims: claims,
expires: DateTime.Now.AddHours(1), // 过期时间:1小时
signingCredentials: creds
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
return Results.Ok(new { Token = tokenString });
});
刚子敲黑板:
这里的 secretKey 是服务器的“底牌”。绝对不能泄露!如果黑客拿到了这个 Key,他就能伪造任意用户的 Token。在生产环境中,一定要放在 appsettings.json 或环境变量里,并且长度要足够长。
五、第二步:配置“安检门”
有了发卡的逻辑,还需要配置中间件,让系统自动验证每个请求带来的 Token。
5.1 注册认证与授权服务
在 Program.cs 的 builder.Services 部分添加:
// 注册认证服务
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// 配置 Token 验证参数
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "MyTodoApp",
ValidAudience = "MyTodoAppUsers",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("This_Is_A_Very_Secret_Key_For_JWT_2024!"))
};
});
// 注册授权服务
builder.Services.AddAuthorization();
5.2 启用中间件
在 app.Build() 之后,确保顺序正确:UseAuthentication 必须在 UseAuthorization 之前。
var app = builder.Build();
app.UseAuthentication(); // 开启认证:识别身份
app.UseAuthorization(); // 开启授权:检查权限
// ... 其他中间件 ...
六、第三步:保护你的 API
现在安检门装好了,我们给之前的 Todo 接口加上“锁”。
6.1 普通保护:登录才能访问
在 Minimal API 中,使用 RequireAuthorization() 扩展方法。
// 只有带了有效 Token 才能访问
app.MapGet("/todos", async (AppDbContext db) =>
{
return await db.Todos.ToListAsync();
}).RequireAuthorization();
6.2 角色保护:管理员才能删除
假设删除操作只有 "Admin" 角色才能执行。
app.MapDelete("/todos/{id}", async (int id, AppDbContext db) =>
{
// ... 删除逻辑 ...
return Results.NoContent();
}).RequireAuthorization("Admin"); // 这是错误的写法!
修正:Minimal API 的角色授权需要配合策略,或者简写为:
// 正确写法:定义一个策略,或者使用 Claims
app.MapDelete("/todos/{id}", async (int id, AppDbContext db) =>
{
// ... 删除逻辑 ...
}).RequireAuthorization(policy => policy.RequireRole("Admin"));
七、实战测试:完整流程演示
让我们用 Postman 或 Swagger 演示整个流程。
场景一:未登录直接访问
- 访问
GET /todos。 - 结果:401 Unauthorized。系统拒绝访问。
场景二:登录获取 Token
- 访问
POST /login,Body 传入{"username": "admin", "password": "123456"}。 - 结果:返回一个 JSON,包含 Token 字符串。
【配图建议 2】 位置:登录接口演示之后。 内容:Postman 截图,展示调用
/login接口后返回的 JSON 响应体,其中包含长字符串 Token。 Alt文本:Postman 调用登录接口成功返回 JWT Token 的截图。
场景三:携带 Token 访问
- 复制刚才的 Token。
- 在请求头中添加:
Authorization: Bearer <你的Token>。(注意 Bearer 后有个空格) - 再次访问
GET /todos。 - 结果:200 OK,成功返回数据!
场景四:权限不足
- 用
gangzi用户登录(角色是 User)。 - 携带 Token 访问
DELETE /todos/1。 - 结果:403 Forbidden。虽然登录了,但权限不够。
【配图建议 3】 位置:权限不足演示之后。 内容:Postman 截图,展示请求头中包含 Authorization Bearer Token,以及访问 Admin 专属接口时返回的 403 Forbidden 状态码。 Alt文本:携带 JWT Token 访问无权限接口返回 403 Forbidden 的截图。
八、刚子小贴士:安全避坑指南
- HTTPS 是必须的:JWT 默认不加密,Token 在网络传输中如果被截获,黑客就能冒充用户。唯一的防御手段就是 HTTPS 加密传输。
- 不要存敏感信息:Payload 任何人都能解码看到,千万别把密码、身份证号存进去。
- Token 有效期:设置合理的过期时间(如 2 小时)。为了体验,通常会配合“Refresh Token”(刷新令牌)机制,这属于进阶话题,感兴趣的同学可以自行研究。
- 配置文件管理:不要把密钥硬编码在代码里,请迁移到
appsettings.json的Jwt:Key配置项中,并使用IConfiguration读取。
九、总结与下篇预告
在这篇文章中,我们完成了系统的安全闭环:
- 理解了 JWT 的原理:Header、Payload、Signature。
- 实现了
/login接口颁发 Token。 - 配置了中间件验证 Token。
- 学会了
RequireAuthorization和基于角色的权限控制。
现在的应用,已经是一个功能完备、安全可靠的系统了!
下一篇预告:
代码写完了,安全也加了,最后一步就是让它真正跑在服务器上。 在第八篇(最终篇)中,刚子将带你进入 容器化部署 的世界。我们将编写 Dockerfile,把应用打包成 Docker 镜像,并在容器中运行。这标志着你从“开发模式”正式迈入“交付模式”。
最后一公里,我们下期见!