.NET6 WebApi JWT用户鉴权实战:原理、配置与避坑指南 简介面向.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是对称算法签发和验签用同一个SecretRS256是非对称私钥签发、公钥验签。自己项目自己签发自己验签我一般直接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 ListClaim { 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能发TokenOpenApiSecurityScheme配置Swagger默认不带Token输入框需要手动配。这个配置藏在AddSwaggerGen里常见做法是注册一个名为Bearer的安全方案再把它设成全局SecurityRequirementusing 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.Emptystring() } }); });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里那些隐藏逻辑最能暴露协议层问题。先启动项目然后登录拿Tokencurl -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 tokenBearer和Token之间有一个空格。写错成Authorization: token token或者干脆不带BearerJwtBearer中间件识别不出来直接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 404Swagger没注册进生产环境现象本地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.GetValuebool(EnableSwagger)) // { // app.UseSwagger(); // app.UseSwaggerUI(); // }还有一个隐蔽场景应用挂在IIS虚拟目录下比如站点路径是http://host/myapiSwaggerUI页面会尝试加载/swagger/v1/swagger.json但真实地址是/myapi/swagger/v1/swagger.json也会404。解决方式是给UseSwaggerUI指定相对路径的SwaggerEndpointc.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并在签发时统一用UtcNowClockSkew TimeSpan.FromSeconds(30) // 测试时甚至可以设为零另一个排查点如果用的是DateTime.Now.AddHours(2)服务器时区是UTC8客户端也是UTC8那没区别。但如果服务器时区被改成UTCDateTime.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 algnone与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); // 重新签发accessTokenrefreshToken也一并换新 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过期走刷新升级后的项目就再也没被脚本和越权请求困扰过希望帮到你。本文还有配套的精品资源点击获取