C# Web开发教程(十)JWT
好的,我来为你详细解释一下 JWT,以及它为什么在现代 Web 开发中如此流行。
什么是 JWT?
JWT,全称 JSON Web Token,是一种开放标准。它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。
简单来说,JWT 是一个数字令牌,它可以证明你是谁的“身份证”。
JWT 的核心结构:三部分
一个 JWT 令牌看起来是一长串看似随机的字符,用点分成三段,例如:
xxxxx.yyyyy.zzzzz
这三段分别是:
- Header
- Payload
- 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,就必须使用服务器持有的密钥重新生成签名。如果客户端篡改了内容,由于没有密钥,生成的签名将无法与服务器验证时生成的签名匹配,令牌就会立即失效。
JWT 如何工作?(认证流程)
我们来看一个典型的 JWT 登录认证流程:
- 用户登录:用户使用用户名和密码登录。
- 服务器验证:服务器验证凭证是否正确。
- 生成 JWT:验证成功后,服务器使用密钥生成一个 JWT,并将其返回给客户端(通常是放在 HTTP Response 的 Body 或 Authorization头中)。
- 客户端存储:客户端(通常是浏览器)收到 JWT,将其保存起来(常见于 localStorage、sessionStorage 或 Cookie 中)。
- 携带令牌请求:之后客户端向服务器发起的每一次请求,都会在 HTTP 请求头(通常是 Authorization: Bearer <token>)中带上这个 JWT。
- 服务器验证令牌:服务器接收到请求后,会:
- 检查 JWT 的签名,验证其是否有效、是否被篡改。
- 检查 exp声明,看令牌是否已过期。
- (可选)检查其他声明,如 iss(签发者)是否合法。
 
- 返回资源:验证通过后,服务器认为请求来自可信的客户端,于是处理请求并返回相应的数据。
为什么 JWT 现在如此流行?(优点)
- 
无状态 
 这是 JWT 最大的优点。服务器不需要在内存或数据库中保存会话状态(Session)。令牌本身包含了所有必要的用户信息。这使得应用非常容易扩展,你可以在多台服务器之间轻松部署,而不用担心会话同步问题。
- 
适合前后端分离与跨域 
 JWT 可以轻松地在不同域名的服务之间传递(通过 CORS 配置),非常适合现代的单页面应用、移动应用和微服务架构。
- 
自包含 
 载荷中可以存储一些常用的用户信息(如用户名、角色),服务器无需每次都为验证身份而查询数据库,减少了数据库的压力。
JWT 的挑战与注意事项(缺点)
- 
令牌无法主动失效 
 一旦签发,在过期时间之前一直有效。如果用户退出登录,或者账号被封禁,服务器无法立即让令牌失效。常见的解决方案有:- 设置较短的过期时间(如 15 分钟)。
- 使用“黑名单”机制,将需要撤销的令牌 ID 存入数据库或 Redis,验证时进行检查(但这又引入了状态,部分抵消了无状态的优点)。
 
- 
安全性依赖密钥 
 签名密钥是 JWT 安全的核心。如果密钥泄露,攻击者可以伪造任何用户的令牌。
- 
令牌体积 
 由于所有信息都存储在令牌里,如果存放的数据过多,会导致令牌体积变大,增加每次网络请求的开销。
- 
存储位置风险 
 如果存储在 localStorage 或 sessionStorage 中,容易受到 XSS 攻击。如果存储在 Cookie 中,需要妥善设置HttpOnly和SameSite等属性来防范 CSRF 攻击。通常建议将 JWT 放在HttpOnlyCookie 中,但对于跨域场景,这可能带来复杂性。
总结
| 特性 | 描述 | 
|---|---|
| 本质 | 一个经过数字签名的、自包含的 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
}
安全注意事项
- 密钥保护:代码中的密钥是硬编码的,实际项目中应从安全配置源获取
- 密钥强度:示例中的密钥足够长且复杂
- 敏感信息:不要在 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();
            }
        }
    }
}
工作流程总结
- 客户端 → 调用 /api/Demo/Login接口进行登录
- 服务器 → 验证用户名密码,生成包含用户信息的 JWT 令牌
- 客户端 → 在后续请求的 Header 中携带 JWT 令牌:Authorization: Bearer
- 服务器 → 自动验证令牌有效性,提取用户信息
[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。
测试流程
- 
登录获取Token POST /api/Auth/login?userName=yzk&password=123456
- 
访问受保护接口 
 在Swagger中点击"Authorize"按钮,输入Bearer {你的Token}
- 
测试角色权限 - 使用yzk用户(有admin角色)可以访问 /api/Demo/admin-only
- 使用test用户(只有user角色)无法访问该接口
 
- 使用yzk用户(有admin角色)可以访问 
- 
测试令牌撤销 POST /api/Auth/revoke/yzk使用admin权限调用此接口后,之前发放的所有yzk用户的令牌都会失效 
- 
测试用户禁用 POST /api/Auth/disable/test调用后test用户的所有令牌都会失效 
这个实现完整包含了文档中提到的所有功能,特别是JWT版本控制机制,可以有效解决JWT无法主动撤销的问题。
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号