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 中,需要妥善设置 HttpOnlySameSite 等属性来防范 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  清安宁  阅读(10)  评论(0)    收藏  举报