简介:.NET6平台下的WebApi开发中,使用JWT进行用户鉴权是一项关键实践。该资源面向具有一定C#基础、希望快速掌握JWT认证与Swagger联调的.NET开发者,提供了一套可直接运行的测试工程,完整演示从用户登录获取令牌,到携带Authorization头访问受保护接口的全过程。包体内共68个文件,以dll、cs、json等为主,并包含sln解决方案与csproj工程文件,便于直接打开编译运行;整体仅1.43MB,轻量实用。项目按AuthenticationModel、AuthenticationService、AuthenticationOperation等层次划分,清晰展示数据模型、业务逻辑与操作入口的职责边界;同时预置Swagger UI,通过AddSwaggerGen配置JWT认证选项,帮助开发者直观调试接口并验证[Authorize]特性效果。目前已有4502人学习下载,适合需要快速搭建安全WebApi或理解JWT鉴权机制的中初级.NET开发者,可作为二次开发与权限扩展的基础模板。
1. 为什么 .NET6 WebApi 接口要自己做 JWT 鉴权
你拿到一个 .NET6 WebApi 项目,接口能跑,但全部裸奔:用 Postman 改一下请求里的用户 ID,就能看到别人的订单数据,这种项目上线就是事故。JWT 是这一类无状态接口最常见的用户鉴权方案:登录时签发一个 Token,后续请求带上它,服务端验签后认可身份,不需要 Session、不需要粘性会话,多个接口实例之间天然互通。它适合从零搭 WebApi 权限体系、把老项目从 Session 登录改造成 Token 登录,以及需要在不同服务之间传递用户身份的团队。标题里的"测试源码"不是某份开源包,而是鉴权链路本身要有一组可复现的验证代码:登录脚本、带 Token 请求、状态码断言,没有它们你无法证明接口真的被保护住了。下面的内容从原理讲到实现,再把测试方法和续签、排错一次性说清楚。
2. JWT 鉴权原理:Token 结构、token 和 jwt 的区别与 .NET6 中间件选型
2.1 JWT 三段结构里,哪一段是给后端看的
JWT 是一串用点号切开的三段字符:Header.Payload.Signature。Header 是开头那一段 JSON,写签名算法和类型,通常是{"alg":"HS256","typ":"JWT"}。Payload 是中间段,JSON 里放 claims,也就是你想让后端信任的用户属性,比如用户 ID、角色、部门。Signature 是最后一段,用约定好的密钥把前两段内容做哈希计算,保证任何一处改动都能在验签时被发现。
一个要先说透的点:JWT 是签名不是加密。三段内容全部是 Base64Url 编码,任何拿到 Token 的人不需要密钥就能把 Payload 解出来读。所以密码、身份证号、手机号这类敏感数据不要放进 Payload,放进去了等于明文贴在请求头上。对 .NET6 后端来说,真正可信的数据是验签通过后的那份 claims;没通过验签的 Token 在中间件层面就被拦掉,控制器里拿到的 User 对象一定来自已验证的内容。
2.2 token 和 jwt 的区别:为什么别把两者划等号
"token 和 jwt 的区别"是 JWT 类接口下检索量不低的关键词,很多人把 Token 直接当成 JWT 来理解。Token 是个宽泛称呼:Session ID、随机字符串、OAuth access token 都叫 Token;JWT 只是 Token 里的一种结构化格式,把信任信息编码进字符串本身。传统随机 Token 不透明,服务端必须查 Session 或 Redis 才知道它代表谁;JWT 自包含,验签通过后直接从 Payload 读出身份,不用查存储。
这个区别直接决定 .NET6 里的选型。接口要横向扩容、多个实例共享流量时,JWT 能省掉集中式会话存储这一层;如果你明确需要"踢人下线、让某个 Token 立刻失效",JWT 天生不擅长,签发出去的服务端没有状态可查,除非自己维护黑名单。先问业务场景接不接受无状态,再决定要不要用 JWT,而不是看别人都在用就跟着上。
2.3 .NET6 里 JwtBearer 与 ASP.NET Core Identity 的职责分工
另一个高频困惑是"用了 JWT 还要不要搭 Identity"。两者管的不是同一层:ASP.NET Core Identity 负责用户存储、密码哈希、角色管理、登录锁定这些账号体系能力;JwtBearer 中间件只负责把 Authorization 请求头里的 Bearer Token 解析出来并验签,再把合法身份放进 HttpContext.User。Identity 不是 JWT 的前置条件,可以只有一张用户表加密码哈希就签发 JWT;接入 Identity 后把用户 ID 和角色写进 Claim 也可以。
| 方案 | 身份信息存在哪 | 多实例部署 | 主动撤销 | .NET6 对应组件 |
|---|---|---|---|---|
| Session | 服务端内存或共享存储 | 需共享存储或粘性会话 | 删 Session 即可 | 内置 Session 中间件 |
| 随机 Token | 服务端或缓存 | 需查存储 | 删记录即可 | 自定义过滤器 |
| JWT | Token 本身 | 无需共享状态 | 需额外黑名单 | JwtBearer |
.NET6 里中间件注册顺序有硬性要求:UseAuthentication 必须在 UseAuthorization 之前,且都在 MapControllers 之前。顺序写反的典型症状是接口加了 [Authorize] 却一路放行,或者全部 401,查业务日志找不出任何原因:
var app = builder.Build(); app.UseAuthentication(); // 先认证:把请求头里的 JWT 解析成 User app.UseAuthorization(); // 后授权:判断 User 是否满足策略 app.MapControllers(); app.Run();顺序说明:认证阶段解决"你是谁",授权阶段解决"你配不配"。两个中间件一前一后串起来,少注册任何一个,[Authorize] 都不会按预期工作。这块先立住,后面所有的排错都会回到这个顺序上。
3. .NET6 WebApi 启用 JWT:配置项、中间件注册与最小可运行代码
3.1 创建项目并安装 JwtBearer 包
最小工程用命令行一把建完:
dotnet new webapi -n JwtDemo cd JwtDemo dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer第一条命令生成 .NET6 的 WebApi 模板,第二条进入项目,第三条把认证中间件包拉进来。JwtBearer 依赖 Authentication.Core,会顺带引入 IdentityModel.Tokens.Jwt,后者负责 JWT 的生成与解析,不需要单独再加装。模板自带的 WeatherForecast 控制器只是为了验证脚手架能跑,做鉴权演示时建议删掉,避免测试请求误打到示例接口上,干扰状态码判断。
3.2 appsettings.json 的 JWT 配置:Issuer、Audience、Key 怎么填
接着在 appsettings.json 加一节独立的 JWT 配置:
{ "Jwt": { "Issuer": "JwtDemo.Api", "Audience": "JwtDemo.Client", "SecretKey": "JwtDemo-Dev-Key-ChangeMe-At-Least-32Bytes", "ExpiresMinutes": 30 } }参数含义:Issuer 是签发方标识,Audience 是合法的调用方标识,验签时 Token 里的iss、aud和这两个值对不上就判定无效。SecretKey 是 HMAC 对称签名密钥,开发环境可以用上面的占位字符串,但准备发布 webapi 项目时一定要换成随机生成的 32 字节以上密钥,并放到环境变量里。.NET6 配置系统支持Jwt__SecretKey双下划线写法,它自动映射到Jwt:SecretKey节点,这样生产环境的 appsettings.json 里可以不出现明文密钥,代码一行不用改。
3.3 Program.cs 里注册认证与授权
Program.cs 是 .NET6 时代的入口,最小可运行版本如下:
using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); var jwtSection = builder.Configuration.GetSection("Jwt"); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtSection["Issuer"], ValidateAudience = true, ValidAudience = jwtSection["Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSection["SecretKey"])), ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1) }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();逻辑说明:AddAuthentication 声明默认认证方案为 JwtBearer,控制器上的 [Authorize] 不指定方案时走这里。AddJwtBearer 里的 TokenValidationParameters 是整套鉴权规则的核心,决定哪些字段被校验、密钥是什么、过期容差多少。最后三段管道注册顺序错一处,鉴权效果就完全不对。各配置项管辖的边界整理成下表:
| 参数 | 作用 | 常见误配 |
|---|---|---|
| ValidateIssuer | 校验 Token 的iss是否等于 ValidIssuer | 开了校验却忘记填 ValidIssuer |
| ValidateAudience | 校验aud是否匹配调用方 | 多个服务共用一个 aud |
| ValidateIssuerSigningKey | 用配置的密钥验签 | 设为 false 等于没鉴权 |
| ValidateLifetime | 校验exp过期时间 | 漏设,过期 Token 照样通行 |
| ClockSkew | 时钟偏移容差 | 留在默认 5 分钟不做调整 |
3.4 两个容易翻车的参数边界:ClockSkew 与密钥长度
3.4.1 ClockSkew 不该用默认值
ClockSkew 是给服务器时钟不同步留的缓冲,默认是 5 分钟。意思是 Token 过期后最多还能再用 5 分钟,不少生产事故的根因就是这个默认值:你以为 Token 已经失效,灰度环境却还在放行。内部系统直接把 ClockSkew 压到 30 秒到 1 分钟,外部系统留 2 分钟足够,不要放任默认值。
3.4.2 HMAC 密钥长度不足的隐性问题
SymmetricSecurityKey 对 HS256 有最小长度要求:256 位,也就是 32 字节。密钥写短了,AddJwtBearer 注册时不一定报错,真实请求进来验签才会抛异常,表现为登录成功但访问接口一直 500。生成密钥用RandomNumberGenerator.GetBytes(32)取随机字节再转 Base64,比手敲一串看得过去的字符串可靠得多。
注意: 配置项里的 Validate 系列开关少一个,JWT 鉴权就是不完整的。最典型的翻车写法是 ValidateIssuerSigningKey = false,等于把 JWT 当透明字符串用,任何人都能伪造身份。
4. 用户鉴权核心实现:登录签发 Token 与接口级授权策略
4.1 登录接口:校验用户后生成 JWT
用户鉴权的最小闭环是:登录接口收到用户名密码,校验通过后签发 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/[controller]")] public class AuthController : ControllerBase { private readonly IConfiguration _config; public AuthController(IConfiguration config) { _config = config; } [HttpPost("login")] [AllowAnonymous] public IActionResult Login(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.NameIdentifier, "10001"), new Claim(ClaimTypes.Role, "admin"), new Claim("dept", "rd") }; var jwtSection = _config.GetSection("Jwt"); var securityKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSection["SecretKey"])); var credentials = new SigningCredentials( securityKey, SecurityAlgorithms.HmacSha256); var expiredAt = DateTime.Now.AddMinutes( Convert.ToDouble(jwtSection["ExpiresMinutes"])); var token = new JwtSecurityToken( issuer: jwtSection["Issuer"], audience: jwtSection["Audience"], claims: claims, notBefore: DateTime.Now, expires: expiredAt, signingCredentials: credentials); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token), expires = expiredAt }); } } public record LoginRequest(string UserName, string Password);参数说明:JwtSecurityToken 的 expires 决定 Token 有效时长,notBefore 指生效时间,取当前时间即可。WriteToken 把对象转成三段字符串,这是唯一需要返回给客户端的凭据。登录失败统一返回 401 且不区分"用户不存在"和"密码错误",防止账号枚举。示例里是明文比对,真实的密码校验至少用 BCrypt 或 PBKDF2 哈希,不做哈希的密码存储属于安全事故。
4.2 把角色和用户 ID 放进 Claim,并在接口里取出来
Claim 是 JWT 里描述用户属性的键值对。实际项目最少放用户主键、登录名、角色,多租户系统再加租户 ID。角色字段建议用 ClaimTypes.Role,这样可以直接配 [Authorize(Roles = "admin")] 的写法,不用自己写判断。取值从控制器的 User 属性拿,不要去重新解析 Token:
[ApiController] [Route("api/[controller]")] [Authorize] public class OrderController : ControllerBase { [HttpGet] public IActionResult Get() { var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var role = User.FindFirst(ClaimTypes.Role)?.Value; var dept = User.FindFirst("dept")?.Value; return Ok(new { userId, role, dept, message = "已通过 JWT 鉴权,按用户身份返回数据" }); } [Authorize(Roles = "admin")] [HttpDelete("{id:int}")] public IActionResult Delete(int id) { return Ok(new { deleted = id }); } }逻辑说明:控制器上的 [Authorize] 拦截未认证请求,方法级的 [Authorize(Roles = "admin")] 在认证通过后继续检查角色,不满足返回 403。User 属性是中间件验签后写入的 claims,不要再自己解 Header。这也是把信息编码进 JWT 的好处,接口层不需要查库就能拿到用户身份。
4.2.1 取不到角色时的第一排查点
如果登录时放了 ClaimTypes.Role,接口里 User.FindFirst(ClaimTypes.Role) 却返回 null,先检查 MapInboundClaims。.NET6 默认会把入站 claim 名做映射,短 claim 名(role、sub)被转成 URI 长名。解决办法是在 AddJwtBearer 里设置options.MapInboundClaims = false;,同时在 TokenValidationParameters 里指定RoleClaimType = "role"、NameClaimType = "name",角色和用户名的取用才会一致。这个坑表现为"Swagger 调试正常,换到 .NET 客户端就取不到",排查成本不低,配置时一并处理掉最省事。
4.3 自定义授权策略:接口权限不只有角色一种
角色满足不了所有场景,比如"研发部门的管理员只能操作本部门数据"。常见做法是自定义策略加 Handler:
public class DeptRequirement : IAuthorizationRequirement { } public class DeptHandler : AuthorizationHandler<DeptRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, DeptRequirement requirement) { var dept = context.User.FindFirst("dept")?.Value; if (dept == "rd") { context.Succeed(requirement); } return Task.CompletedTask; } } // Program.cs 注册 builder.Services.AddAuthorization(options => { options.AddPolicy("DeptPolicy", policy => policy.Requirements.Add(new DeptRequirement())); }); builder.Services.AddSingleton<IAuthorizationHandler, DeptHandler>();逻辑说明:Handler 里的 context.User 是验签后的 ClaimsPrincipal,只从 claims 判断,不要在这里查数据库,保持鉴权过程无状态。没有调用 Succeed 时请求默认 403,所以部门判断条件要写完整,漏掉 else 分支不会报错但会静默拒绝。策略适合多接口复用的权限规则;单接口的简单校验用 [Authorize(Roles = ...)] 已经足够,过度设计反而难维护。
4.4 401 和 403 的排查边界
| 状态码 | 含义 | 排查方向 |
|---|---|---|
| 401 | 未认证 | 请求头是否带 Bearer,Token 是否过期 |
| 403 | 已认证但无权限 | 角色名是否一致,策略 Handler 是否 Succeed |
| 200 但 User 里取不到值 | 认证过了,claim 映射不对 | MapInboundClaims、RoleClaimType 配置 |
看到 403 先对比角色字符串,JWT 角色名大小写敏感,admin 和 "Admin" 是两个值。看到 401 先抓请求头看 Authorization 是不是Bearer加三段式 Token,格式不对中间件直接拒收。这套排查表可以直接写进测试用例的断言里,比人肉盯着 Swagger 翻高效得多。
5. 测试源码组织:Swagger、curl 与 winform 客户端测 WebApi 接口
5.1 Swagger 里配置 Authorization,先跑通最小流程
.NET6 的 WebApi 模板自带 Swagger,但默认不带鉴权按钮。需要在 AddSwaggerGen 里注册 Bearer 方案:
using Microsoft.OpenApi.Models; builder.Services.AddSwaggerGen(options => { var scheme = new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header }; options.AddSecurityDefinition("Bearer", scheme); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty<string>() } }); });逻辑说明:AddSecurityDefinition 告诉 Swagger UI 在页面上渲染一个 Authorize 按钮;AddSecurityRequirement 把该方案挂到每个接口上,请求时自动带 Authorization 头。Scheme 写 "bearer" 小写,Swagger 会正确拼成Bearer <token>。调试顺序固定为:先调用 login 接口拿 Token,点 Authorize 粘贴进去,再调业务接口;顺序反过来的话业务接口永远 401。
5.2 发布后的接口用 curl 和 Postman 验证
本地跑通不代表发布 webapi 项目之后还正常,发布环境的配置可能被环境变量改写,密钥可能被替换。最省事的线上验证是一组 curl:
# 1. 登录拿 token curl -X POST "https://api.example.com/api/Auth/login" \ -H "Content-Type: application/json" \ -d '{"userName":"admin","password":"123456"}' # 2. 带 token 调用受保护接口 curl -X GET "https://api.example.com/api/Order" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."参数说明:第二步把第一步返回的 Token 原样粘到 Bearer 后面。生产环境不要用示例密码,登录接口必须加失败次数限制,否则等于把爆破入口敞开着。Postman 里可以把 Token 存成集合变量,登录脚本里写pm.collectionVariables.set("token", json.token),请求头引用Authorization: Bearer {{token}},后续接口全部自动带上,整套测试省掉反复复制粘贴。
5.3 winform 中测试 webapi 接口的 HttpClient 写法
内部工具经常用 winform 直接测 WebApi:窗口输入账号密码,登录后调用受保护接口展示结果。这类客户端的关键是 HttpClient 只建一个,登录成功后把 Token 写进默认请求头:
public partial class MainForm : Form { private static readonly HttpClient http = new HttpClient { BaseAddress = new Uri("https://api.example.com") }; private async void btnLogin_Click(object sender, EventArgs e) { var login = new { userName = "admin", password = "123456" }; var response = await http.PostAsJsonAsync("api/Auth/login", login); var result = await response.Content .ReadFromJsonAsync<LoginResult>(); http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Bearer", result.token); MessageBox.Show("登录成功,有效期至 " + result.expires); } private async void btnCallApi_Click(object sender, EventArgs e) { using var resp = await http.GetAsync("api/Order"); var body = await resp.Content.ReadAsStringAsync(); txtResult.Text = $"{(int)resp.StatusCode}\r\n{body}"; } } public class LoginResult { public string token { get; set; } public DateTime expires { get; set; } }逻辑说明:PostAsJsonAsync 把匿名对象序列化后发给 login 接口,ReadFromJsonAsync 反序列化返回体拿到 Token。DefaultRequestHeaders.Authorization 是全局的,winform 单用户场景合适;多用户模拟就得每个用户建独立 HttpClient,否则后登录的人会把前一个人的 Token 顶掉。按钮事件用 async/await 而不是 .Result,界面才不会在请求期间冻结。
5.4 测试源码的目录结构与断言建议
测试源码建议独立建项目,不塞进主工程。目录按被测对象划分:
tests/JwtDemo.Api.Tests/ AuthControllerTests.cs # 登录成功、密码错误、缺 Token OrderControllerTests.cs # 有 Token 200、无 Token 401、非 admin 403 TestAuthHandler.cs # 生成合法 Token 的辅助类TestAuthHandler 里放一个静态方法,用与 Program.cs 完全相同的配置签发 Token,测试用例直接复用,不重复写构造逻辑。断言不要只看状态码,下面几个场景要写进用例里:
| 测试场景 | 预期结果 | 断言重点 |
|---|---|---|
| 不带 Token 请求业务接口 | 401 | 状态码与 WWW-Authenticate 头 |
| 带过期 Token 请求 | 401 | 状态码,且不进入控制器 |
| 普通角色调用 admin 接口 | 403 | 状态码,业务数据未返回 |
| 合法 admin 调用 | 200 | 返回体中的用户 ID 与 Token 一致 |
提示: 测试工程和生产配置的 TokenValidationParameters 要完全一致,否则会出现测试全过、发布后全部 401 的经典事故。
6. 进阶:token 续签与 JWT 漏洞总结及防护清单
6.1 token 续签的两种做法:滑动过期与刷新令牌
JWT 的 expires 一到就失效,客户端被迫重新登录。内部系统我优先用滑动过期:业务接口收到合法 Token 后判断剩余有效期,低于阈值就签发一个全新 Token 随响应返回,前端收到后替换本地存储。代价是控制器里多一次时间比较,适合用户长期在线的后台管理系统。
6.1.1 刷新令牌的落地结构
需要严格控制权限的系统用 refresh_token 方案:登录时发一对,access_token 短效,refresh_token 长效 7 天。刷新令牌存进数据库并记录用户、设备、签发时间。关键在于续签接口——校验旧刷新令牌有效后立即吊销它,返回全新的一对:
[HttpPost("refresh")] public IActionResult Refresh(RefreshRequest request) { // 校验 refresh_token 是否有效且未吊销 var valid = _tokenStore.FindValid(request.RefreshToken, out var userId); if (!valid) { return Unauthorized(new { message = "刷新令牌无效或已过期" }); } // 吊销旧令牌,防止重放 _tokenStore.Revoke(request.RefreshToken); var newAccessToken = BuildAccessToken(userId); var newRefreshToken = Guid.NewGuid().ToString("N"); _tokenStore.Save(newRefreshToken, userId, DateTime.UtcNow.AddDays(7)); return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken }); }参数说明:refresh_token 用不透明随机串而不是 JWT,因为服务端要能主动吊销它。存储时间统一用 UTC,避免服务器与客户端时区差导致续签提前失效。refresh_token 泄露风险高于 access_token,续签接口必须走 HTTPS,这是底线。
6.2 jwt 漏洞总结:算法混淆、密钥过弱与校验项缺失
JWT 相关的漏洞大多不在 .NET6 框架本身,而在配置和用法上。检索"jwt 漏洞总结"看到的高频案例集中在以下几类:
| 漏洞 | 成因 | 防护手段 |
|---|---|---|
| alg=none 攻击 | 服务端接受无签名 Token | 显式指定算法,如 HmacSha256 |
| RS256 置换 HS256 | 用公钥当 HMAC 密钥验签 | 算法白名单,区分对称与非对称密钥 |
| 密钥过弱或硬编码 | 弱密钥可被离线爆破 | 32 字节以上随机密钥,密钥走环境变量 |
| 校验项缺失 | ValidateLifetime 未开 | 按 3.3 的配置逐项核对 |
.NET6 里固定算法的方式是签发与验签两侧都写死 SecurityAlgorithms.HmacSha256,不要依赖默认推断。更严格的团队会用自定义 IssuerSigningKeyResolver 做算法白名单,把允许的算法数组放进配置,白名单之外的 alg 一律拒绝。想系统练手可以跑 OWASP WebGoat 的 JWT 关卡,它把 alg=none、密钥爆破、算法置换逐个场景化,做完再回来看自己的配置,印象会深很多。
6.3 加固后的自测清单
按顺序过一遍,全部满足才算这套 JWT 用户鉴权真正立住:
- 不带 Token 访问受保护接口,确认 401
- 用错误密钥签一个 Token,确认 401
- 修改合法 Token Payload 里的角色字段,确认 401 而不是 403
- 等 Token 过期后再请求,确认 401 且响应没有明显变慢
- winform 客户端登录后连续调用接口,确认每次请求都带 Authorization 头
- 用 5.2 的 curl 在发布 webapi 项目的服务器上重跑一遍,确认环境变量没有改掉密钥
最后一条最容易翻车:开发用密钥 A、生产用密钥 B 切换时,老 Token 全部失效,用户被迫全体重新登录。发布前先在测试环境完成密钥轮换并跑一遍上述清单,确认老的 access_token 即时失效、刷新令牌能续上,再去动生产配置。
本文还有配套的精品资源,点击获取