C# Web开发教程(十)JWT

好的,我来为你详细解释一下 JWT,以及它为什么在现代 Web 开发中如此流行。

什么是 JWT?

JWT,全称 JSON Web Token,是一种开放标准。它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。

简单来说,JWT 是一个数字令牌,它可以证明你是谁的“身份证”。


JWT 的核心结构:三部分

一个 JWT 令牌看起来是一长串看似随机的字符,用点分成三段,例如:
xxxxx.yyyyy.zzzzz

这三段分别是:

  1. Header
  2. Payload
  3. Signature

1. Header

头部通常由两部分组成:

  • 令牌类型:这里是 JWT。
  • 签名算法:如 HMAC SHA256 或 RSA。

例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个 JSON 会被 Base64Url 编码,形成 JWT 的第一部分。

2. Payload

载荷是令牌的核心,包含了你要传递的“声明”。声明就是关于实体(通常是用户)和其他数据的语句。

声明有三种类型:

  • 注册声明:预定义但不强制使用的标准字段,如:
    • iss:签发者
    • exp:过期时间
    • sub:主题
    • aud:受众
  • 公共声明:可以自定义的字段,但为了避免冲突,应在 IANA JSON Web Token 注册表中定义。
  • 私有声明:提供方和消费者共同约定的自定义字段,用于在双方之间共享信息。

一个典型的 Payload 可能如下:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

同样,这个 JSON 也会被 Base64Url 编码,形成 JWT 的第二部分。

重要提示:Payload 只是被编码,并没有加密。任何人都可以解码看到其中的内容。因此,绝对不要在 Payload 中存放密码等敏感信息。

3. Signature

签名是 JWT 最关键的部分,它保证了令牌的完整性和真实性,防止被篡改。

签名的生成方式如下:

Signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret)

这个过程是:

  • 将编码后的 Header 和 Payload 用点连接起来。
  • 使用 Header 中指定的算法(如 HS256)和一个只有服务器才知道的密钥对这个字符串进行加密签名。

签名的妙处在于:如果有人修改了 Header 或 Payload,就必须使用服务器持有的密钥重新生成签名。如果客户端篡改了内容,由于没有密钥,生成的签名将无法与服务器验证时生成的签名匹配,令牌就会立即失效。

flowchart TD A[开始: 用户登录成功] --> B[创建 Header] B --> C[创建 Payload] C --> D[Base64Url 编码 Header] D --> E[Base64Url 编码 Payload] E --> F{拼接编码后的内容} F --> G["base64Header + '.' + base64Payload"] G --> H[使用密钥和算法生成签名] H --> I[Base64Url 编码签名] I --> J{拼接完整 JWT} J --> K["encodedHeader + '.' + <br>encodedPayload + '.' + <br>encodedSignature"] K --> L[返回 JWT 给客户端] L --> M[结束] subgraph B [Header 创建] B1[定义令牌类型: JWT] --> B2[定义签名算法: HS256/RSA等] end subgraph C [Payload 创建] C1[添加标准声明<br>exp, iat, iss等] --> C2[添加自定义数据<br>userId, username, roles等] end subgraph H [签名生成] H1["获取密钥<br>(服务器保密)"] --> H2["使用指定算法<br>HMAC-SHA256等"] --> H3["对拼接字符串<br>进行加密签名"] end

JWT 如何工作?(认证流程)

我们来看一个典型的 JWT 登录认证流程:

  1. 用户登录:用户使用用户名和密码登录。
  2. 服务器验证:服务器验证凭证是否正确。
  3. 生成 JWT:验证成功后,服务器使用密钥生成一个 JWT,并将其返回给客户端(通常是放在 HTTP Response 的 Body 或 Authorization 头中)。
  4. 客户端存储:客户端(通常是浏览器)收到 JWT,将其保存起来(常见于 localStorage、sessionStorage 或 Cookie 中)。
  5. 携带令牌请求:之后客户端向服务器发起的每一次请求,都会在 HTTP 请求头(通常是 Authorization: Bearer <token>)中带上这个 JWT。
  6. 服务器验证令牌:服务器接收到请求后,会:
    • 检查 JWT 的签名,验证其是否有效、是否被篡改。
    • 检查 exp 声明,看令牌是否已过期。
    • (可选)检查其他声明,如 iss(签发者)是否合法。
  7. 返回资源:验证通过后,服务器认为请求来自可信的客户端,于是处理请求并返回相应的数据。
flowchart TD A[客户端] --> B[发送请求携带 JWT] B --> C["Authorization: Bearer <token>"] C --> D[服务器接收请求] D --> E{提取 JWT Token} E --> F[拆分三部分: Header.Payload.Signature] F --> G[验证 Signature] G --> H{签名验证是否通过?} H -- 不通过 --> I[返回 401 Unauthorized] I --> J[认证失败] H -- 通过 --> K[解码 Payload] K --> L[验证过期时间 exp] L --> M{令牌是否过期?} M -- 是 --> N[返回 401 Token Expired] N --> J M -- 否 --> O[验证其他声明 iss, aud 等] O --> P{其他验证是否通过?} P -- 不通过 --> Q[返回 403 Forbidden] Q --> J P -- 通过 --> R[从 Payload 提取用户信息] R --> S["用户ID, 角色, 权限等"] S --> T[处理业务逻辑] T --> U[返回请求的资源] U --> V[认证成功] subgraph G [签名验证过程] G1[使用服务器密钥] --> G2[重新计算签名] G2 --> G3[与令牌中的签名对比] end subgraph R [用户信息提取] R1[读取用户ID sub] --> R2[读取用户角色 roles] R2 --> R3[读取其他自定义声明] end style A fill:#e1f5fe style U fill:#e8f5e9 style J fill:#ffebee style V fill:#e8f5e9

为什么 JWT 现在如此流行?(优点)

  1. 无状态
    这是 JWT 最大的优点。服务器不需要在内存或数据库中保存会话状态(Session)。令牌本身包含了所有必要的用户信息。这使得应用非常容易扩展,你可以在多台服务器之间轻松部署,而不用担心会话同步问题。

  2. 适合前后端分离与跨域
    JWT 可以轻松地在不同域名的服务之间传递(通过 CORS 配置),非常适合现代的单页面应用、移动应用和微服务架构。

  3. 自包含
    载荷中可以存储一些常用的用户信息(如用户名、角色),服务器无需每次都为验证身份而查询数据库,减少了数据库的压力。


JWT 的挑战与注意事项(缺点)

  1. 令牌无法主动失效
    一旦签发,在过期时间之前一直有效。如果用户退出登录,或者账号被封禁,服务器无法立即让令牌失效。常见的解决方案有:

    • 设置较短的过期时间(如 15 分钟)。
    • 使用“黑名单”机制,将需要撤销的令牌 ID 存入数据库或 Redis,验证时进行检查(但这又引入了状态,部分抵消了无状态的优点)。
  2. 安全性依赖密钥
    签名密钥是 JWT 安全的核心。如果密钥泄露,攻击者可以伪造任何用户的令牌。

  3. 令牌体积
    由于所有信息都存储在令牌里,如果存放的数据过多,会导致令牌体积变大,增加每次网络请求的开销。

  4. 存储位置风险
    如果存储在 localStorage 或 sessionStorage 中,容易受到 XSS 攻击。如果存储在 Cookie 中,需要妥善设置 HttpOnly 和 SameSite 等属性来防范 CSRF 攻击。通常建议将 JWT 放在 HttpOnly Cookie 中,但对于跨域场景,这可能带来复杂性。

总结

特性 描述
本质 一个经过数字签名的、自包含的 JSON 数据令牌。
核心 签名保证了令牌的完整性和真实性。
优点 无状态、易于扩展、适合跨域和微服务。
缺点 无法主动注销、安全性高度依赖密钥、有潜在的体积问题。

JWT 是一种非常强大的身份认证技术,但它并非银弹。在选择是否使用 JWT 时,需要根据你的应用场景(如对无状态的需求、安全性要求、令牌管理复杂度等)来权衡利弊。对于大多数需要无状态扩展和跨域支持的现代 Web 应用来说,它无疑是一个极佳的选择。

  • JWT实例演示
- PM> Install-Package System.IdentityModel.Tokens.JWT 
// 新建控制台应用测试,Program.cs

using Microsoft.IdentityModel.Tokens; 
using System.IdentityModel.Tokens.Jwt; 
using System.Security.Claims; 
using System.Text; 


var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, "6")); 
claims.Add(new Claim(ClaimTypes.Name, "yzk"));
claims.Add(new Claim(ClaimTypes.Role, "User")); 
claims.Add(new Claim(ClaimTypes.Role, "Admin"));
claims.Add(new Claim("PassPort", "E90000082")); 
/*
claims.Add(new Claim(ClaimTypes.NameIdentifier, "1"));
claims.Add(new Claim(ClaimTypes.Name, "root"));
claims.Add(new Claim(ClaimTypes.Role, "Admin"));*/

// key的长度要够,如果太短,编译的时候会报错...
string key = "fasdfad&9045dafz222#fadsfhsakhfdksajfkdjsadjfkasdfjlksadfsf@0232";
DateTime expires = DateTime.Now.AddDays(1);
byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims,
    expires: expires, signingCredentials: credentials);
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
Console.WriteLine(jwt);

1. 引入必要的命名空间

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
  • Microsoft.IdentityModel.Tokens:提供安全令牌相关的核心类
  • System.IdentityModel.Tokens.Jwt:专门处理 JWT 令牌的类
  • System.Security.Claims:处理声明(Claims)的类
  • System.Text:用于编码处理

2. 创建声明集合

var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, "6"));
claims.Add(new Claim(ClaimTypes.Name, "yzk"));
claims.Add(new Claim(ClaimTypes.Role, "User"));
claims.Add(new Claim(ClaimTypes.Role, "Admin"));
claims.Add(new Claim("PassPort", "E90000082"));

声明说明:

  • ClaimTypes.NameIdentifier:用户唯一标识符 → "6"
  • ClaimTypes.Name:用户名 → "yzk"
  • ClaimTypes.Role:用户角色(添加了两次,表示用户有多个角色)→ "User" 和 "Admin"
  • 自定义声明 "PassPort" → "E90000082"

这将在 JWT Payload 中生成:

{
  "nameid": "6",
  "unique_name": "yzk",
  "role": ["User", "Admin"],
  "PassPort": "E90000082",
  // ... 其他标准声明
}

3. 配置密钥和过期时间

string key = "fasdfad&9045dafz222#fadsfhsakhfdksajfkdjsadjfkasdfjlksadfsf@0232";
DateTime expires = DateTime.Now.AddDays(1);
  • key:用于签名的密钥(实际项目中应从安全配置读取)
  • expires:令牌过期时间(1天后)

4. 准备签名凭证

byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
  • 将字符串密钥转换为字节数组
  • 创建对称安全密钥对象
  • 创建签名凭证,指定使用 HMAC SHA256 算法

5. 创建令牌描述符

var tokenDescriptor = new JwtSecurityToken(claims: claims,
    expires: expires, signingCredentials: credentials);

创建包含所有必要信息的令牌配置对象。

6. 生成 JWT 字符串

string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
Console.WriteLine(jwt);
  • 使用 JwtSecurityTokenHandler 将令牌描述符转换为实际的 JWT 字符串
  • 输出生成的 JWT

生成的 JWT 结构示例

生成的 JWT 将类似于:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2IiwibmFtZSI6Inl6ayIsInJvbGUiOiJBZG1pbiIsImlhdCI6MTYx...,...signature...

// 完整字符串如下

eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjYiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoieXprIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIlVzZXIiLCJBZG1pbiJdLCJQYXNzUG9ydCI6IkU5MDAwMDA4MiIsImV4cCI6MTc2MTc4ODc2NH0.0SqE0umOfvJz2Ifkg7BZmj92yhoJrp_O2Nb88vocthU

对应的 Payload 结构:

{
  "nameid": "6",
  "unique_name": "yzk",
  "role": ["User", "Admin"],
  "PassPort": "E90000082",
  "nbf": 1633042800,
  "exp": 1633129200,
  "iat": 1633042800
}

安全注意事项

  1. 密钥保护:代码中的密钥是硬编码的,实际项目中应从安全配置源获取
  2. 密钥强度:示例中的密钥足够长且复杂
  3. 敏感信息:不要在 JWT 中存储密码等敏感信息,因为 Payload 只是 Base64 编码,不是加密

这段代码完整演示了在 .NET 中生成 JWT 的标准流程,包括声明配置、密钥管理和令牌生成。

JWT解码

  • 实例如下
using System.Text;

string jwt = Console.ReadLine()!;
string[] segments = jwt.Split('.');
string head = JwtDecode(segments[0]);
string payload = JwtDecode(segments[1]);
Console.WriteLine("--------head--------");
Console.WriteLine(head);
Console.WriteLine("--------payload--------");
Console.WriteLine(payload);
string JwtDecode(string s)
{
    s = s.Replace('-', '+').Replace('_', '/');
    switch (s.Length % 4)
    {
        case 2:
            s += "==";
            break;
        case 3:
            s += "=";
            break;
    }
    var bytes = Convert.FromBase64String(s);
    return Encoding.UTF8.GetString(bytes);
}
  • 解码的结果如下
eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjYiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoieXprIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIlVzZXIiLCJBZG1pbiJdLCJQYXNzUG9ydCI6IkU5MDAwMDA4MiIsImV4cCI6MTc2MTc4ODc2NH0.0SqE0umOfvJz2Ifkg7BZmj92yhoJrp_O2Nb88vocthU
--------head--------
{"alg":"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256","typ":"JWT"}
--------payload--------
{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":"6","http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":"yzk","http://schemas.microsoft.com/ws/2008/06/identity/claims/role":["User","Admin"],"PassPort":"E90000082","exp":1761788764}

代码整体功能

这段代码的主要功能是:手动解析 JWT 令牌,解码并显示其头部(Header)和载荷(Payload)部分的内容。


代码逐行解释

1. 读取 JWT 输入

string jwt = Console.ReadLine()!;

从控制台读取用户输入的 JWT 令牌字符串。

2. 分割 JWT 的三部分

string[] segments = jwt.Split('.');

将 JWT 按点号 . 分割成三部分:

  • segments[0]:Header(头部)
  • segments[1]:Payload(载荷)
  • segments[2]:Signature(签名) - 代码中未使用

3. 解码头部和载荷

string head = JwtDecode(segments[0]);
string payload = JwtDecode(segments[1]);

调用自定义的 JwtDecode 方法分别解码头部和载荷。

4. 输出解码结果

Console.WriteLine("--------head--------");
Console.WriteLine(head);
Console.WriteLine("--------payload--------");
Console.WriteLine(payload);

格式化输出解码后的头部和载荷内容。


JwtDecode 方法详解

这是核心的解码方法,处理 Base64Url 解码:

步骤 1:Base64Url 到 Base64 转换

s = s.Replace('-', '+').Replace('_', '/');

作用:将 Base64Url 编码转换为标准的 Base64 编码。

转换规则:

  • - → +(Base64Url 中的减号对应 Base64 中的加号)
  • _ → /(Base64Url 中的下划线对应 Base64 中的斜杠)

步骤 2:补全填充位

switch (s.Length % 4)
{
    case 2:
        s += "==";
        break;
    case 3:
        s += "=";
        break;
}

作用:为 Base64 字符串补全填充字符 =。

原理:Base64 编码要求字符串长度是 4 的倍数:

  • 长度除 4 余 2:补 2 个 =
  • 长度除 4 余 3:补 1 个 =
  • 长度除 4 余 0:不需要补(代码中未显式处理,因为默认就是正确的)

步骤 3:Base64 解码

var bytes = Convert.FromBase64String(s);
return Encoding.UTF8.GetString(bytes);
  • 将 Base64 字符串转换为字节数组
  • 将字节数组按 UTF-8 编码转换为可读字符串

示例演示

如果输入以下 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2IiwibmFtZSI6Inl6ayIsInJvbGUiOiJBZG1pbiJ9.signature_ignored

运行结果:

--------head--------
{"alg":"HS256","typ":"JWT"}
--------payload--------
{"sub":"6","name":"yzk","role":"Admin"}

重要注意事项

1. 安全性警告

// 这段代码只做演示,实际项目中不要这样验证 JWT!
  • 此代码没有验证签名,无法保证 JWT 的真实性
  • 攻击者可以轻易篡改 Header 和 Payload 并重新编码
  • 生产环境必须使用正规的 JWT 验证库

2. Base64Url 与 Base64 的区别

特性 Base64 Base64Url
字符 62 + -
字符 63 / _
填充 = =(通常省略)
URL安全 不安全 安全

3. 实际应用场景

这种手动解码主要用于:

  • 开发和调试时查看 JWT 内容
  • 理解 JWT 的内部结构
  • 教学演示目的

生产环境中应该使用:

var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(jwt);
// 或者使用 ValidateToken 方法进行完整验证

框架的JWT玩法

  • 在 ASP.NET Core 中使用 JWT 认证的流程

  • 新建API项目,安装JWT库

- Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 6.0.0 // 提供 JWT 令牌的生成和验证功能
  • 新建配置类JWTSettings
namespace WebApplicationAboutJWTConfigRun
{
    public class JWTSettings // 定义 JWT 的配置模型,用于从 appsettings.json 读取配置
    {
        public string SecKey { get; set; }
        public int ExpireSeconds { get; set; }
    }
}
  • Program.cs注册一下
using WebApplicationAboutJWTConfigRun;

var builder = WebApplication.CreateBuilder(args);
......
// JWT配置
builder.Services.Configure<JWTSettings>(builder.Configuration.GetSection("JWT")); // 将配置文件中的 "JWT" 节点绑定到 JWTSettings 类
// JWT认证服务配置: :告诉 ASP.NET Core 如何验证传入的 JWT 令牌
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTSettings>();
    byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SecKey);
    var secKey = new SymmetricSecurityKey(keyBytes);
    x.TokenValidationParameters = new()
    {
        ValidateIssuer = false, // 不验证发行者
        ValidateAudience = false,  // 不验证受众
        ValidateLifetime = true, // 验证令牌有效期
        ValidateIssuerSigningKey = true, // 验证签名密钥
        IssuerSigningKey = secKey // 设置签名密钥
    };
});
......
var app = builder.Build();
// 配置认证(必须放在UseAuthorization之前)
app.UseAuthentication();
app.UseAuthorization();


......

  • appsettings.cs配置私匙
{
  "Logging": {
    ......
  },
......
  "Jwt": {
    "SecKey": "sjdfklasdklfjsaldfjasjoasjkdfjas4554sdfsadfsa45d",
    "ExpireSeconds": 3600   
  }
}

  • API 接口
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace WebApplicationAboutJWTConfigRun.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    // [Authorize] // 先不要认证
    public class DemoController : ControllerBase
    {
        private readonly IOptionsSnapshot<JWTSettings> jwtSettingsOpt;

        public DemoController(IOptionsSnapshot<JWTSettings> jwtSettingsOpt)
        {
            this.jwtSettingsOpt = jwtSettingsOpt; // 通过依赖注入获取 JWT 配置
        }

        [HttpPost]
        public ActionResult<string> Login(string userName,string password)
        {
            if(userName =="yzk" && password == "123456")
            {
            	 // 创建声明(Claims)
                var claims = new List<Claim>();
                claims.Add(new Claim(ClaimTypes.NameIdentifier, "1"));
                claims.Add(new Claim(ClaimTypes.Name, userName));
				 // 准备令牌参数
                string key = jwtSettingsOpt.Value.SecKey;
                DateTime expire = DateTime.Now.AddSeconds(jwtSettingsOpt.Value.ExpireSeconds);
                byte[] secBytes = Encoding.UTF8.GetBytes(key);
                var secKey = new SymmetricSecurityKey(secBytes);
                var credentials = new SigningCredentials(secKey, 
                // 创建并生成令牌
                SecurityAlgorithms.HmacSha256Signature);
                var tokenDescriptor = new JwtSecurityToken(claims: claims, expires: expire, signingCredentials: credentials);
                string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
                return jwt;
            }
            else
            {
                return BadRequest();
            }
        }
    }
}

工作流程总结

  1. 客户端 → 调用 /api/Demo/Login 接口进行登录
  2. 服务器 → 验证用户名密码,生成包含用户信息的 JWT 令牌
  3. 客户端 → 在后续请求的 Header 中携带 JWT 令牌:Authorization: Bearer
  4. 服务器 → 自动验证令牌有效性,提取用户信息

[Authorize]和[AllowAnonymous]特性

  • [Authorize]: 类特性,类下面的所有接口都必须先验证,然后才能进入接口
  • [AllowAnonymous]: 接口特性,不必再验证
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace WebApplicationAboutJWTConfigRun.Controllers
{

    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize] // 类特性
    public class Demo2Controller : ControllerBase
    {
        [HttpGet] // 这个接口必须先验证,否则进不来
        public string Test1()
        {
            var claim = this.User.FindFirst(ClaimTypes.Name);
            return "ok" + claim?.Value;
        }

        [HttpGet] 
        [AllowAnonymous] // 该接口无需验证,直接进
        public string Test2()
        {

            return "666";
        }

    }
}

  • [Authorize(Roles = "admin")]接口特性: 必须是符合条件的角色账户,才能访问该接口
// 原来的接口加一句

public ActionResult<string> Login(string userName,string password)
{
    if(userName =="yzk" && password == "123456")
    {
        var claims = new List<Claim>();
        ......
        claims.Add(new Claim(ClaimTypes.Name, userName));
        claims.Add(new Claim(ClaimTypes.Role,"admin")); // 新增
       ......
        return jwt;
    }
    else
    {
        return BadRequest();
    }
}
[HttpGet]
[Authorize(Roles = "admin")] // 新增角色限制
public string Test3()
{      
    return "333";
}

- 访问: https://localhost:7283/api/Demo2/Test3
- 请求头: Authorization header. Example: 'Bearer 12345abcdef'
Name: Authorization
In: header
Value: ****** // 生成的token
  • 注意事项,如果想让Swagger测试工具,也具有类似postman的部分功能,可以这么做
// Program.cs

......
// 新增配置
builder.Services.AddSwaggerGen(c =>
{
    var scheme = new OpenApiSecurityScheme()
    {
        Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Authorization"
        },
        Scheme = "oauth2",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    };
    c.AddSecurityDefinition("Authorization", scheme);
    var requirement = new OpenApiSecurityRequirement();
    requirement[scheme] = new List<string>();
    c.AddSecurityRequirement(requirement);
});

......

app.Run();

解决JWT无法撤回的问题

  • jwt的缺点
- 到期前,令牌无法被提前撤回。什么情况下需要撤回?用户被删除了、禁用了;令牌被盗用了;多设备登录。
- 需要JWT撤回的场景用传统Session更合适。
- 如果需要在JWT中实现,思路:用Redis保存状态,或者用refresh_token+access_token机制等。

- 本项目思路:在用户表中增加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号;
  每次登录、发放令牌的时候,都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载中;
  当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值自增;
  当服务器端收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果
  JWT令牌中JWTVersion的值小于数据库中JWTVersion的值
  就说明这个JWT令牌过期了。
  • 实现流程如下
- 创建配置类JWTSettings

- 在Program.cs中注册JWT认证服务

- 在appsettings.json中配置JWT

- 创建用户模型(包含JWTVersion)

- 实现登录接口,发放令牌时更新JWTVersion并写入令牌

- 实现一个操作筛选器(Filter)来检查JWT令牌中的JWTVersion是否与数据库中的一致(注册为全局筛选器)

注意:为了简化,我们不会使用真正的数据库,而是使用一个静态列表来模拟用户表。

1. 项目结构和配置

Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
using WebApplicationAboutJWTConfigRun;
using WebApplicationAboutJWTConfigRun.Filters;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

// JWT配置
builder.Services.Configure<JWTSettings>(builder.Configuration.GetSection("JWT"));

// JWT认证服务配置
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTSettings>();
    byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SecKey);
    var secKey = new SymmetricSecurityKey(keyBytes);
    x.TokenValidationParameters = new()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = secKey
    };
});

// Swagger配置
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT Demo API", Version = "v1" });
    
    var scheme = new OpenApiSecurityScheme()
    {
        Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Authorization"
        },
        Scheme = "oauth2",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    };
    c.AddSecurityDefinition("Authorization", scheme);
    var requirement = new OpenApiSecurityRequirement();
    requirement[scheme] = new List<string>();
    c.AddSecurityRequirement(requirement);
});

// 注册全局过滤器
builder.Services.AddControllers(options =>
{
    options.Filters.Add<JWTValidationFilter>();
});

// 模拟用户服务(实际项目中应该使用数据库)
builder.Services.AddSingleton<UserService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// 配置认证(必须放在UseAuthorization之前)
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

2. 配置类

JWTSettings.cs

namespace WebApplicationAboutJWTConfigRun
{
    public class JWTSettings
    {
        public string SecKey { get; set; }
        public int ExpireSeconds { get; set; }
    }
}

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "JWT": {
    "SecKey": "sjdfklasdklfjsaldfjasjoasjkdfjas4554sdfsadfsa45d",
    "ExpireSeconds": 3600
  }
}

3. 用户模型和服务

User.cs

namespace WebApplicationAboutJWTConfigRun
{
    public class User
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public long JWTVersion { get; set; }
        public List<string> Roles { get; set; } = new List<string>();
        public bool IsActive { get; set; } = true;
    }
}

UserService.cs

using System.Collections.Concurrent;

namespace WebApplicationAboutJWTConfigRun
{
    public class UserService
    {
        private readonly ConcurrentDictionary<string, User> _users = new();

        public UserService()
        {
            // 初始化测试用户
            _users["yzk"] = new User
            {
                UserName = "yzk",
                Password = "123456",
                JWTVersion = 1,
                Roles = new List<string> { "admin", "user" },
                IsActive = true
            };
            
            _users["test"] = new User
            {
                UserName = "test",
                Password = "123456",
                JWTVersion = 1,
                Roles = new List<string> { "user" },
                IsActive = true
            };
        }

        public User GetUser(string userName)
        {
            return _users.TryGetValue(userName, out var user) ? user : null;
        }

        public void UpdateUser(User user)
        {
            _users[user.UserName] = user;
        }

        public void DisableUser(string userName)
        {
            if (_users.TryGetValue(userName, out var user))
            {
                user.IsActive = false;
                user.JWTVersion++;
            }
        }

        public void RevokeUserTokens(string userName)
        {
            if (_users.TryGetValue(userName, out var user))
            {
                user.JWTVersion++;
            }
        }
    }
}

4. JWT验证过滤器

JWTValidationFilter.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Claims;

namespace WebApplicationAboutJWTConfigRun.Filters
{
    public class JWTValidationFilter : IActionFilter
    {
        private readonly UserService _userService;

        public JWTValidationFilter(UserService userService)
        {
            _userService = userService;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            // 检查是否有AllowAnonymous特性
            if (context.ActionDescriptor.EndpointMetadata.Any(em => em.GetType() == typeof(Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute)))
            {
                return;
            }

            // 检查用户身份
            var user = context.HttpContext.User;
            if (user.Identity?.IsAuthenticated == true)
            {
                var userName = user.FindFirst(ClaimTypes.Name)?.Value;
                var jwtVersionClaim = user.FindFirst("JWTVersion")?.Value;

                if (!string.IsNullOrEmpty(userName) && long.TryParse(jwtVersionClaim, out var tokenVersion))
                {
                    var dbUser = _userService.GetUser(userName);
                    if (dbUser == null || !dbUser.IsActive || tokenVersion < dbUser.JWTVersion)
                    {
                        context.Result = new UnauthorizedObjectResult("Token已失效,请重新登录");
                        return;
                    }
                }
            }
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // 执行后不做处理
        }
    }
}

5. 控制器

AuthController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace WebApplicationAboutJWTConfigRun.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [AllowAnonymous]
    public class AuthController : ControllerBase
    {
        private readonly IOptionsSnapshot<JWTSettings> _jwtSettingsOpt;
        private readonly UserService _userService;

        public AuthController(IOptionsSnapshot<JWTSettings> jwtSettingsOpt, UserService userService)
        {
            _jwtSettingsOpt = jwtSettingsOpt;
            _userService = userService;
        }

        [HttpPost("login")]
        public ActionResult<string> Login(string userName, string password)
        {
            var user = _userService.GetUser(userName);
            if (user != null && user.Password == password && user.IsActive)
            {
                // 更新JWT版本号
                user.JWTVersion++;
                _userService.UpdateUser(user);

                // 创建声明(Claims)
                var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, user.UserName),
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim("JWTVersion", user.JWTVersion.ToString())
                };

                // 添加角色声明
                foreach (var role in user.Roles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, role));
                }

                // 准备令牌参数
                string key = _jwtSettingsOpt.Value.SecKey;
                DateTime expire = DateTime.Now.AddSeconds(_jwtSettingsOpt.Value.ExpireSeconds);
                byte[] secBytes = Encoding.UTF8.GetBytes(key);
                var secKey = new SymmetricSecurityKey(secBytes);
                var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
                
                var tokenDescriptor = new JwtSecurityToken(claims: claims, expires: expire, signingCredentials: credentials);
                string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
                
                return Ok(new { Token = jwt, Expires = expire, Version = user.JWTVersion });
            }
            else
            {
                return BadRequest("用户名或密码错误");
            }
        }

        [HttpPost("revoke/{userName}")]
        [Authorize(Roles = "admin")]
        public IActionResult RevokeUserTokens(string userName)
        {
            _userService.RevokeUserTokens(userName);
            return Ok($"用户 {userName} 的所有令牌已被撤销");
        }

        [HttpPost("disable/{userName}")]
        [Authorize(Roles = "admin")]
        public IActionResult DisableUser(string userName)
        {
            _userService.DisableUser(userName);
            return Ok($"用户 {userName} 已被禁用");
        }
    }
}

DemoController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace WebApplicationAboutJWTConfigRun.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class DemoController : ControllerBase
    {
        [HttpGet("test1")]
        public string Test1()
        {
            var nameClaim = this.User.FindFirst(ClaimTypes.Name);
            var versionClaim = this.User.FindFirst("JWTVersion");
            return $"OK - 用户名: {nameClaim?.Value}, JWT版本: {versionClaim?.Value}";
        }

        [HttpGet("test2")]
        [AllowAnonymous]
        public string Test2()
        {
            return "这个接口无需认证";
        }

        [HttpGet("admin-only")]
        [Authorize(Roles = "admin")]
        public string AdminOnly()
        {
            return "只有管理员可以访问这个接口";
        }

        [HttpGet("user-info")]
        public IActionResult GetUserInfo()
        {
            var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList();
            return Ok(claims);
        }
    }
}

6. 使用说明

启动项目

运行项目后,访问 https://localhost:PORT/swagger 来测试API。

测试流程

  1. 登录获取Token

    POST /api/Auth/login?userName=yzk&password=123456
    
  2. 访问受保护接口
    在Swagger中点击"Authorize"按钮,输入 Bearer {你的Token}

  3. 测试角色权限

    • 使用yzk用户(有admin角色)可以访问 /api/Demo/admin-only
    • 使用test用户(只有user角色)无法访问该接口
  4. 测试令牌撤销

    POST /api/Auth/revoke/yzk
    

    使用admin权限调用此接口后,之前发放的所有yzk用户的令牌都会失效

  5. 测试用户禁用

    POST /api/Auth/disable/test
    

    调用后test用户的所有令牌都会失效

这个实现完整包含了文档中提到的所有功能,特别是JWT版本控制机制,可以有效解决JWT无法主动撤销的问题。

posted @ 2025-10-29 09:04  安_宁  阅读(167)  评论(0)    收藏  举报