☰
.NET6 WebApi JWT用户鉴权实战:原理、配置与避坑指南
2026/10/1 10:58:23 网站建设 项目流程

简介:面向.NET6平台Web API开发者的JWT用户鉴权完整示例源码包,解决前后端分离场景下用户身份验证与接口授权问题。资源演示了登录成功后由服务端生成并颁发令牌,后续请求携带该令牌即可通过校验,免去重复提交用户名密码。内容涵盖令牌生成与验证配置、Swagger的JWT认证接入、控制器[Authorize]特性鉴权等关键环节,并包含AuthenticationModel、AuthenticationOperation、AuthenticationService等分层模块,便于理解项目结构。压缩包共68个文件,以C#源码、配置json、程序集dll及依赖文件为主,另含解决方案与项目文件,整体仅1.43MB,下载后即可编译学习。已有4503人学习使用。资源适合正在学习WebApi安全认证、需要快速集成JWT或希望参考分层实践的初中级开发者。

1. 为什么WebApi接口要上JWT:先解决"谁在调我的接口"

我经手过的前后端分离项目,接口上线前必做同一件事:用户鉴权。原来用Session一套逻辑在服务端渲染时代没毛病,换到Vue、小程序、App混合调用后,Cookie在很多场景下不好使,跨域、移动端、横向扩容都得额外照顾。于是JWT成了最常见的选择:登录后服务端签发一个自包含的Token,客户端存着,每次请求放进Authorization头,服务端验签通过就放行,全程不查会话表。这篇以.NET6平台的WebApi项目为例,把JWT用户鉴权从原理讲到落地,再从Swagger联调讲到测试源码的组织方式,新手能照着跑通,熟手能避掉那些隐蔽的坑。

2. JWT三段结构、HS256与选型边界:动手之前先把签名逻辑搞懂

2.1 Header、Payload、Signature:一个Token拆开看

JWT不是一个加密协议,它只是三段Base64Url字符串用点号拼起来:header.payload.signature。一个典型的HS256签名Token长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzMzNjQ3MzY2fQ.签名段

前两段不用任何密钥就能解码。Header解码后是:

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

Payload解码后是:

{"unique_name":"admin","role":"admin","exp":1733647366}

第三段签名才是关键:把前两段用点号拼起来,再用密钥走HMACSHA256算出摘要。Token里任何一段被改,签名都对不上,服务端直接拒绝。在.NET里拿到Token后,用JwtSecurityTokenHandler可以瞬间拆开,适合调试时确认"里面到底装了什么":

var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzMzNjQ3MzY2fQ.签名段"; var handler = new JwtSecurityTokenHandler(); // 只读解析,不验签,调试用足够 if (handler.CanReadToken(token)) { var jwt = handler.ReadJwtToken(token); Console.WriteLine($"签发者: {jwt.Issuer}"); Console.WriteLine($"受众: {jwt.Audiences.FirstOrDefault()}"); Console.WriteLine($"过期: {jwt.ValidTo}"); foreach (var claim in jwt.Claims) { Console.WriteLine($"{claim.Type} = {claim.Value}"); } }

这里的CanReadToken只检查格式,ReadJwtToken不验签,所以任何Token都能被解析出来。想验证签名是否有效,必须靠后面配置的TokenValidationParameters。调试JWT时先做这一步,能快速定位"到底是Token内容不对,还是验签配置不对"。另外,看到的是Base64Url解码后的原文,如果Code Review时发现有人把密码、身份证号放进Claim,必须拦下来,这等于把明文发给所有拿到Token的人。

2.2 HS256和RS256怎么选:对称还是非对称

签名算法决定了signature段怎么算。HS256是对称算法,签发和验签用同一个Secret;RS256是非对称,私钥签发、公钥验签。自己项目自己签发自己验签,我一般直接HS256,配置里只有一把Key,简单高效。只有当Token要给多个下游微服务验签,而你不希望把私钥散到每个服务里时,才值得上RS256。对比下来差异很清楚:

对比项HS256RS256
密钥形式一个对称密钥私钥签发,公钥验签
性能快慢一些
配置复杂度低中
适用场景单体、单服务、自家签发自家验微服务多调用方、第三方认证中心
泄密影响密钥泄露即可伪造任意Token私钥泄露才可伪造,公钥泄了没关系

.NET里HmacSha256要求密钥至少256位,否则运行时报SecurityKeyTooShortException。很多人第一次配置Token就栽在这里,以为随便写个字符串就行,实际长度不够直接抛异常,后面避坑章再展开。另一点建议:选HS256时不要把密钥硬编码在代码里,appsettings.json也尽量不要提交进Git仓库,用用户机密或环境变量覆盖,血泪经验,线上密钥泄露一次就够你加班一夜。

2.3 为什么不用Session:无状态到底解决了什么

Session方案在WebApi上最大的痛点是横向扩容和跨域。我遇到过部署两台WebApi实例的场景,Session默认存进程内,用户第一次请求落在A实例,第二次落在B实例,Session就丢了,用户被莫名其妙踢下线。解决办法要么负载均衡做会话粘滞,要么把Session搬到Redis,但这些全是额外设施。JWT把用户身份全部塞进Token本身,任意实例拿到Token都能独立验签,不需要共享会话存储。

JWT的代价也很明确:服务端不能主动吊销一个还没过期的Token,除非引入黑名单,那又等于回到"服务端存状态"。所以"后台封号立即生效""用户改密码后所有Token立即失效"这类诉求,纯JWT做不干净,需要后面讲到的续签策略兜底。选型时先想清楚:你的用户体系是"登录即可、过期重登",还是"必须随时踢人"。前者JWT轻量直接,后者建议AccessToken加RefreshToken组合。还有一点别忽略:Payload只是Base64Url编码,不是加密,任何能拿到Token的人都能看到里面的Claims,所以敏感信息只能放在服务端,不能放Token里。

2.4 最小依赖清单:只加两个NuGet包

在.NET6里做JWT鉴权不需要引入一堆包。WebApi模板自带Swashbuckle,鉴权相关只需要Microsoft.AspNetCore.Authentication.JwtBearer,签发Token用System.IdentityModel.Tokens.Jwt,它其实是JwtBearer的传递依赖,但为了写代码时命名空间不缺,建议项目文件里显式声明。新建.NET6 WebApi项目后,用命令安装即可:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

或者直接在NuGet管理器里搜JwtBearer,版本选6.0.x。装完后Program.cs里需要出现Microsoft.AspNetCore.Authentication.JwtBearer和Microsoft.IdentityModel.Tokens两个命名空间,前者负责认证中间件,后者提供SymmetricSecurityKey、SigningCredentials这些签名用的类型。记住,这一步装的是"验签和装配"的能力,真正签发Token还要靠System.IdentityModel.Tokens.Jwt里的JwtSecurityTokenHandler。

2.5 Claim映射:为什么登录后取不到用户名

经常有人登录成功后写User.Identity.Name,发现是null。原因:JwtBearer默认把JWT里的unique_name或sub映射到ClaimTypes.Name,把role映射到ClaimTypes.Role。如果你把用户名放在自定义Claim如userName里,框架不知道该拿哪个当身份,User.Identity.Name自然为空。解决的常见做法是在TokenValidationParameters里指定映射:

options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "userName", RoleClaimType = "role" };

也就是说,签发Token时用new Claim("userName", ...)看起来无所谓,但验签时必须告诉框架"哪一个Claim是用户名、哪一个Claim是角色"。否则后面写[Authorize(Roles = "admin")]永远匹配不上,接口一直403,排查半天也找不到原因。这个映射问题在JWT落地里出现频率极高,提前配好能省很多调试时间。

3. .NET6中落地JWT鉴权:服务注册、登录接口与Swagger配置

3.1 Program.cs注册认证服务:TokenValidationParameters的七个开关

在.NET6里,整个鉴权链路是从Program.cs开始的。先把JWT认证服务注册进去,我一般把配置拆成五组:签名密钥、签发者、受众、过期时间、时钟偏移。其中密钥组必须开ValidateIssuerSigningKey,否则签名不验证,等于Token谁都能伪造。完整的最小配置如下:

using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { // 1. 必须验证签名,否则任何人都能伪造Token ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])), // 2. 验证签发者 ValidateIssuer = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], // 3. 验证受众 ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"], // 4. 验证过期时间,并允许30秒时钟偏移 ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30) }; }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();

这里有个隐藏点:UseAuthentication必须在UseAuthorization之前,顺序反了的话,请求根本不会执行认证,所有[Authorize]接口一律401,而且Swagger里看不出任何异常。新手常见做法是先写UseAuthorization,再去查为什么没生效,结果折腾半天是顺序问题。另外,TokenValidationParameters里那三个Validate开关全开之后,Issuer、Audience、Key三项配置都必须对得上,任何一项不匹配都会验签失败,后面我会讲怎么看失败日志。

3.2 登录接口:签发Token的核心代码

登录接口是整个鉴权链路的起点。先校验验证码,再核对用户名密码,都通过后生成Token返回给前端。下面这个AuthController是完整的可复现版本:

using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; [ApiController] [Route("api/auth")] public class AuthController : ControllerBase { private readonly IConfiguration _config; public AuthController(IConfiguration config) { _config = config; } [HttpPost("login")] public IActionResult Login([FromBody] LoginRequest request) { // 真实项目这里先校验图形验证码或短信验证码,防止脚本刷接口 if (request.UserName != "admin" || request.Password != "123456") { return Unauthorized(new { message = "用户名或密码错误" }); } var claims = new List<Claim> { new Claim(ClaimTypes.Name, request.UserName), new Claim(ClaimTypes.Role, "admin"), new Claim("uid", "10001") }; var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config["Jwt:Key"])); var credentials = new SigningCredentials( key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddHours(2), signingCredentials: credentials); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token), expiresAt = token.ValidTo }); } } public class LoginRequest { public string UserName { get; set; } public string Password { get; set; } }

关键点在于这个new Claim(ClaimTypes.Name, ...),它和前面的NameClaimType映射是配套的。如果这里用了ClaimTypes.Name,那TokenValidationParameters的NameClaimType可以不用改,默认就能识别;如果这里用了自定义的userName,那映射必须配上,否则User.Identity.Name又取不到。expires建议用DateTime.UtcNow,跨时区项目不会因为本地时间引发奇奇怪怪的偏差。

3.3 保护接口的三种写法:整控制器、按角色、按策略

Token签发出来后,保护接口只需一个[Authorize]特性。三种常见粒度,按需求选:

[ApiController] [Route("api/[controller]")] public class WeatherController : ControllerBase { // 方式一:登录用户就能访问 [HttpGet] [Authorize] public IActionResult Get() { return Ok(new { data = "登录后可访问" }); } // 方式二:指定角色,适合后台管理接口 [HttpGet("admin")] [Authorize(Roles = "admin")] public IActionResult GetAdmin() { return Ok(new { data = "仅管理员可访问" }); } // 方式三:整个控制器统一要求登录,个别匿名接口用[AllowAnonymous] [HttpGet("health")] [AllowAnonymous] public IActionResult Health() { return Ok(new { status = "ok" }); } }

建议把[Authorize]直接放在控制器类上,一劳永逸,否则漏标一个接口就是安全隐患。我审计项目时最怕看到那种每个方法都手动加特性的写法,新增接口忘了加,直接裸奔上线。还有一种常见需求是多个角色取并集,[Authorize(Roles = "admin,editor")]用逗号分隔就行。角色名匹配依赖前面说的RoleClaimType,默认是ClaimTypes.Role,用自定义role时记得在TokenValidationParameters里指定。

3.4 让Swagger能发Token:OpenApiSecurityScheme配置

Swagger默认不带Token输入框,需要手动配。这个配置藏在AddSwaggerGen里,常见做法是注册一个名为Bearer的安全方案,再把它设成全局SecurityRequirement:

using Microsoft.OpenApi.Models; builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My WebApi", Version = "v1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "Bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "粘贴JWT Token,不需要加Bearer前缀" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty<string>() } }); });

Type用SecuritySchemeType.Http比ApiKey更稳,Swagger会按"Authorization: Bearer "的格式把Token塞进去。如果用ApiKey方案,Swagger可能把Token放进QueryString里,JwtBearer默认不从QueryString读Token,接口照样401,这是Swagger联调里一个很容易踩的现实坑。

配置完重新跑项目,每个接口右上角应该出现一把锁。点开输入Token后,Swagger发请求时会自动带Authorization头。到这里,登录、签发、保护、联调已经全通了,接下来是测试源码的整理。

4. 测试源码怎么组织:用curl、HttpClient小工具验证整条链路

4.1 先用curl把协议跑通:JWT发包格式长什么样

测试JWT接口的第一步,永远是curl。因为curl没有浏览器缓存、没有Swagger里那些隐藏逻辑,最能暴露协议层问题。先启动项目,然后登录拿Token:

curl -s -X POST http://localhost:5000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"userName":"admin","password":"123456"}'

正常情况下返回一段JSON,里面是token和expiresAt两个字段。拿到Token后访问受保护接口,注意Authorization头的发包格式,这是JWT联调里最容易出错的位置:

# 用jq从登录响应里提取token字段 TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"userName":"admin","password":"123456"}' | jq -r '.token') # 带Token访问受保护接口 curl -i http://localhost:5000/api/weather \ -H "Authorization: Bearer $TOKEN"

正确格式一定是Authorization: Bearer <token>,Bearer和Token之间有一个空格。写错成Authorization: token <token>或者干脆不带Bearer,JwtBearer中间件识别不出来,直接401。如果把Token误放到请求体或QueryString里,默认配置同样不认。这套curl命令可以直接复制到测试文档里,也适合写进CI流水线做冒烟验证。

4.2 一个.NET6控制台测试程序:端到端验证整条链路

curl跑通后,我会在仓库里单独留一个TestConsole项目,用HttpClient把登录、带Token请求、无Token请求三条路径都测一遍。这样新同事拉代码后不用开Swagger,一条命令就能确认鉴权链路是好的。核心代码不长:

using System.Net.Http.Headers; using System.Text; using System.Text.Json; var client = new HttpClient { BaseAddress = new Uri("http://localhost:5000") }; // 1. 登录,拿Token var loginBody = new StringContent( JsonSerializer.Serialize(new { userName = "admin", password = "123456" }), Encoding.UTF8, "application/json"); var loginResponse = await client.PostAsync("/api/auth/login", loginBody); loginResponse.EnsureSuccessStatusCode(); var json = await loginResponse.Content.ReadAsStringAsync(); var token = JsonDocument.Parse(json) .RootElement.GetProperty("token") .GetString(); Console.WriteLine($"Token: {token}"); // 2. 带Token访问受保护接口,期待200 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var dataResponse = await client.GetAsync("/api/weather"); Console.WriteLine($"带Token状态码: {dataResponse.StatusCode}"); Console.WriteLine(await dataResponse.Content.ReadAsStringAsync()); // 3. 不带Token访问,期待401 client.DefaultRequestHeaders.Authorization = null; var unAuthResponse = await client.GetAsync("/api/weather"); Console.WriteLine($"无Token状态码: {unAuthResponse.StatusCode}");

小技巧是把这段代码放进.NET6控制台项目,直接Run,三步验证下来链路通没通一目了然。HttpClient的BaseAddress配置好之后,后面的Post和Get全部用相对路径,不用每次拼完整URL,这个习惯能减少很多低级错误。如果公司里有Postman或Apifox,也可以把同样的场景存成Collection,效果等同,但控制台程序的好处是不依赖图形界面,服务器上也能跑。

4.3 解析Token验证过期时间:不引入第三方库的解码方法

测试中最常要验证的是"Token里到底有没有正确的过期时间"。网上很多工具能解JWT,但命令行下最快的方式是自己写几行解码函数。JWT的Payload只是Base64Url编码,不是加密,所以解码完全不需要引入额外包:

static string DecodeJwtPayload(string token) { var parts = token.Split('.'); if (parts.Length != 3) { throw new ArgumentException("不是合法的JWT格式"); } // Base64Url转标准Base64 string base64 = parts[1] .Replace('-', '+') .Replace('_', '/'); switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Encoding.UTF8.GetString(Convert.FromBase64String(base64)); }

解码后用JsonDocument解析,直接看exp的值。exp是Unix时间戳,用DateTimeOffset.FromUnixTimeSeconds换算成本地时间,一眼就能确认Token有效期对不对。这个函数我在测试项目里保留着,排查"Token过期了但客户端说没过期"这类问题时尤其好用。注意不要对Signature段做任何解码,它是二进制哈希,没有可读内容。

4.4 集成测试和手工验证的取舍

再往后可以上xUnit加WebApplicationFactory写集成测试,但那套玩意儿初始化成本高,还要处理数据库、测试环境配置,中小项目维护起来反而得不偿失。我的判断标准很简单:如果团队里有人会改鉴权代码,那就值得上集成测试;如果鉴权逻辑半年不动一次,curl加控制台测试程序足够。常见做法是把TestConsole项目留在解决方案里,但不要把它加进WebApi项目的引用,避免测试代码污染生产项目。测试项目的csproj里把输出类型设为Exe,用Top-level Statement跑,结构清晰还不占额外依赖。

5. JWT避坑清单:Swagger 404、时钟偏移、弱密钥与发包格式

5.1 发布WebApi项目后Swagger 404:Swagger没注册进生产环境

现象:本地VS调试一切正常,发布WebApi项目到IIS或Windows服务后,访问/swagger/v1/swagger.json提示not found,界面直接404。原因九成是Program.cs里把UseSwagger包进了环境判断:

if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

发布时ASPNETCORE_ENVIRONMENT变成Production,这段代码跳过,Swagger整个没注册。解决:把环境判断去掉,或者改成配置开关:

app.UseSwagger(); app.UseSwaggerUI(); // 如果实在不想在生产暴露,用配置控制 // if (builder.Configuration.GetValue<bool>("EnableSwagger")) // { // app.UseSwagger(); // app.UseSwaggerUI(); // }

还有一个隐蔽场景:应用挂在IIS虚拟目录下,比如站点路径是http://host/myapi,SwaggerUI页面会尝试加载/swagger/v1/swagger.json,但真实地址是/myapi/swagger/v1/swagger.json,也会404。解决方式是给UseSwaggerUI指定相对路径的SwaggerEndpoint:c.SwaggerEndpoint("swagger/v1/swagger.json", "WebApi V1")。排查时分两步:先直接访问swagger.json看有没有JSON返回,没有就是注册问题,有但样式不对才是路径问题。

5.2 401但Token明明没过期:时钟偏移和服务器时间

现象:客户端解析Token看到exp是两小时后,但请求接口一直401,日志提示token expired。原因有两个方向:一是服务器和客户端时钟差太大,二是Token签发时用了DateTime.Now而服务器时区混乱。JwtBearer默认允许5分钟时钟偏移,如果两边时间差超过这个窗口,有效的Token也会被判定为过期。解决:TokenValidationParameters里显式设置ClockSkew,并在签发时统一用UtcNow:

ClockSkew = TimeSpan.FromSeconds(30) // 测试时甚至可以设为零

另一个排查点:如果用的是DateTime.Now.AddHours(2),服务器时区是UTC+8,客户端也是UTC+8,那没区别。但如果服务器时区被改成UTC,DateTime.Now会比客户端慢8小时,Token实际有效期被拉长或缩短,出现"假过期"或"假有效"。所以签发端和验签端都用DateTime.UtcNow是最省心的,别给时区问题留机会。

5.3 密钥太短:HS256对密钥长度的硬性要求

现象:启动项目时抛SecurityTokenInvalidSignatureException或SecurityKeyTooShortException,提示密钥位数不够。我见过有人用"jwt-key"这种短字符串当密钥,HS256要求密钥至少256位,也就是32字节,中文UTF-8下每个汉字占3字节,9个汉字其实也够了,但ASCII下的9个字符只有72位,直接报错。解决:换成足够长的随机字符串,建议直接用32字节以上的随机数:

# 生成一个足够长的随机密钥 openssl rand -base64 48

把生成结果放进appsettings.json的Jwt:Key位置。这里顺便把网上那些JWT漏洞总结里最经典的一条说透:弱密钥配合暴力破解,攻击者可以反推出密钥,然后伪造任意身份的Token,等于整个用户体系被击穿。所以密钥不仅要长,还要随机,不要用"123456"这类可猜的字符串。如果公司有密钥管理系统,优先把它接进来,没条件就至少保证每个环境用不同的随机密钥。

5.4 自定义Claim导致登录后身份为空:NameClaimType映射

现象:登录接口明明是成功的,Token也能解析出userName和role,但控制器里User.Identity.Name是null,[Authorize(Roles = "admin")]也不生效。原因:签发Token时用了自定义Claim名,但TokenValidationParameters没有告诉框架哪个是用户名、哪个是角色。解决:二选一,要么签发时统一用ClaimTypes.Name和ClaimTypes.Role,要么在验签配置里手动映射:

new TokenValidationParameters { NameClaimType = "userName", RoleClaimType = "role" }

建议直接采用后者,因为很多代码是历史项目留下来的,签发端改名会影响所有已签发的Token,而映射只是验签端一个配置项。这个坑的特点是:Swagger联调时接口能通,但拿不到用户身份,很容易让人怀疑是HttpContext.User的使用姿势问题,实际就是映射没配。排查时先在登录接口里把Token打出来,再用前面那个解码函数看Claim名,和映射一比就清楚了。

5.5 alg=none与kid:两句保命安全提醒

JWT攻击里最经典的是把Header里的alg改成none,让服务端跳过签名验证;还有一类是密钥混淆,把RS256算法改成HS256,用公钥当密钥验签。在.NET的JwtBearer里,默认配置会校验签名,不会因为Token里写了个none就放飞自我,前提是你没有自己写SignatureValidator偷偷把它关掉。搜项目里有没有SignatureValidator这个配置项,如果有自定义实现,建议直接删掉,用默认的TokenValidationParameters就足够了,不要为了兼容某个老系统去手写验签逻辑,调试时图省事,线上就是漏洞。

再说kid。如果认证中心会在Header里带kid,用来选择验签密钥,你要知道JwtBearer默认不会按kid自动选密钥,需要基于IssuerSigningKeyResolver做多密钥支持。反过来,如果项目只有一个固定密钥,就别在Token里加kid字段,免得多个密钥轮换时旧Token验签链路混乱。多密钥轮换是个复杂话题,建议只在有独立认证中心的场景下才碰。

6. Token续签的三种做法:AccessToken、RefreshToken与黑名单

JWT一签发出,服务端就无法主动注销,这是无状态方案的天然副作用。实际项目里"用户退出登录后Token继续有效7小时"这种事不能接受,续签策略必须提前想好。常见做法有三种:

方案思路优点缺点
滑动过期AccessToken在每次请求时若剩余时间不足一半,就在响应里重签新Token代码简单,无状态无法强制下线,前端要配合响应头
RefreshToken登录时同时发短期Token和长期RefreshToken,过期后用RefreshToken换新支持吊销,安全边界清晰服务端要存RefreshToken,多一套表和接口
黑名单把注销的Token放进Redis,验签时先查黑名单可即时踢人放弃了无状态,每次请求多一次Redis查询

如果项目只服务于自家前端,AccessToken设30分钟到2小时,加滑动过期就够。如果面向很多客户端或涉及支付、管理后台这类安全敏感场景,RefreshToken是标准答案。一个最小化的刷新接口长这样:

[HttpPost("refresh")] public IActionResult Refresh([FromBody] RefreshRequest request) { // 这里必须查数据库或Redis,确认refreshToken有效、未过期、未吊销 var stored = _refreshTokenService.Validate(request.RefreshToken); if (stored == null) { return Unauthorized(new { message = "refresh token无效或已吊销" }); } // 刷新令牌一次性使用:用旧token换新token,旧token立即标记为已使用 _refreshTokenService.MarkAsUsed(request.RefreshToken); // 重新签发accessToken,refreshToken也一并换新 var newAccessToken = _tokenService.CreateAccessToken(stored.UserId); var newRefreshToken = _tokenService.CreateRefreshToken(stored.UserId); return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken }); }

RefreshToken的存储建议用Redis加过期时间,天然支持过期和吊销,比数据库更符合"高并发读取少写"的特征。我自己的习惯是:测试环境用滑动过期减少复杂度,生产环境直接上RefreshToken,黑名单只在前两种方案都覆盖不了的极端封号需求时才引入。有一点必须提醒:RefreshToken的过期时间不要设太长,7到30天足够,它一旦泄露就能反复换新Token,安全等级比AccessToken更高。整个鉴权链路到这里就闭环了——登录发Token,请求带Token,过期走刷新,升级后的项目就再也没被脚本和越权请求困扰过,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询