ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

.NET6 JWT鉴权实战:从Swagger调试到Refresh Token落地

.NET6 JWT鉴权实战:从Swagger调试到Refresh Token落地 简介本资源是一份面向.NET开发者特别是初学者与中级工程师的Web API安全开发实践项目聚焦于在.NET 6平台下集成JWT实现用户身份鉴权与接口保护。项目完整覆盖JWT令牌生成、签发验证、Swagger交互式文档配置、控制器授权控制等核心环节并提供可直接运行的源码工程适用于API安全机制学习、企业级鉴权模块搭建及课程实训参考。压缩包共68个文件含11个C#业务类如AuthenticationController、AuthenticationService、14个JSON配置文件含appsettings及NuGet缓存、18个DLL依赖库及解决方案文件.sln整体体积仅1.43MB结构清晰、模块解耦明确便于快速理解分层设计逻辑。目前已有4503人学习下载读者可直接导入Visual Studio运行调试掌握从登录接口返回Token、前端携带Bearer Token访问受保护API、以及Swagger中一键认证测试的全流程实践能力。1. 这不是“加个[Authorize]就完事”的JWT样板——它是一套能直接跑通、带Swagger交互测试、含三层分离结构的.NET6 WebAPI鉴权落地包你刚在VS里新建一个.NET6 WebAPI项目敲完[Authorize]F5一跑——401 Unauthorized满屏飞Swagger里点“Authorize”按钮填了Bearer token还是被拒翻了三篇博客发现全是“安装NuGet包→配置AddJwtBearer→写Login方法”但没人告诉你ClockSkew设成0秒会卡死本地调试也没人提ValidateIssuerSigningKeytrue却漏配SecurityKey时连启动都报错。这个源码包就是我去年在给医疗IoT平台做设备接入网关时从零手撕、反复压测、最终上线跑满18个月的JWT鉴权骨架它不只生成token还内置了密码强度校验PasswordValidator、登录失败锁定FailedLoginTracker、token续签逻辑RefreshTokenHandler且所有鉴权中间件都按.NET6 Minimal Hosting模型重写没用过时的Startup.cs。如果你正卡在“JWT能生成但验不过”“Swagger传token无效”“用户登出后token仍可用”这三座大山里这份源码就是你的撬棍——它包含3个明确分层的csprojModel/Service/Controller、完整appsettings.json密钥配置项、可直接运行的AuthenticationController.cs和配套的Postman/Swagger测试用例。新手照着AuthenticationService.sln双击打开就能跑熟手能直接拆出AuthenticationService项目里的JwtTokenGenerator类塞进自己项目替换掉那堆玄学配置。2. 从零搭起JWT管道为什么选System.IdentityModel.Tokens.Jwt而非Microsoft.AspNetCore.Authentication.JwtBearer2.1 选型真相Minimal Hosting下JwtBearer中间件的隐性陷阱.NET6强制推行Minimal Hosting模型WebApplication.CreateBuilder而老派services.AddAuthentication().AddJwtBearer()在Program.cs里写起来看似简洁实则埋了三个深坑第一坑TokenValidationParameters必须显式注入——AddJwtBearer默认不加载IssuerSigningKey若只配ValidIssuer和ValidAudience运行时抛IDX10240: Unable to validate signature错误日志却只显示“token invalid”根本看不出是密钥没加载第二坑ClockSkew默认5分钟——开发机时间比服务器快3分钟本地调试永远401第三坑RequireHttpsMetadata true在HTTP开发环境必炸——Swagger走http://localhost:5000时中间件直接拒绝解析token。本源码包绕过AddJwtBearer改用System.IdentityModel.Tokens.Jwt原生库手动验证把JwtSecurityTokenHandler封装进AuthenticationService的ValidateToken方法。这样做的好处是所有验证逻辑可控、可断点、可单元测试。比如ValidateToken里强制ValidateLifetime true且ClockSkew TimeSpan.Zero再配合DateTime.UtcNow硬校验彻底消灭时区/时间差导致的验签失败。2.2 手动注入JWT验证服务代码即配置// Program.cs 中的服务注册关键 var builder WebApplication.CreateBuilder(args); // 1. 从appsettings.json读取JWT配置 var jwtSettings builder.Configuration.GetSection(JwtSettings); builder.Services.ConfigureJwtSettings(jwtSettings); // 2. 注册自定义JWT服务非AddJwtBearer builder.Services.AddScopedIJwtTokenService, JwtTokenService(); builder.Services.AddSingletonJwtSecurityTokenHandler(); // 复用单例提升性能 // 3. 注册AuthenticationService含密码校验、token生成等 builder.Services.AddScopedAuthenticationService();提示JwtSettings类必须严格匹配appsettings.json结构否则ConfigurationBinder绑定失败会导致NullReferenceException。本包中JwtSettings.cs定义如下public class JwtSettings { public string? Key { get; set; } // 必须是Base64编码的32字节密钥如Convert.ToBase64String(new byte[32]) public string? Issuer { get; set; } // 如 https://auth.example.com public string? Audience { get; set; } // 如 https://api.example.com public int AccessTokenExpirationMinutes { get; set; } 30; public int RefreshTokenExpirationDays { get; set; } 7; }2.3 生成JWT令牌别再用硬编码字符串拼接本包AuthenticationService.GenerateAccessToken方法采用标准JwtSecurityToken构造而非字符串拼接。关键参数全由JwtSettings驱动public string GenerateAccessToken(string userId, string userName, IEnumerablestring roles) { var jwtSettings _configuration.GetSection(JwtSettings).GetJwtSettings(); var key new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); var creds new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token new JwtSecurityToken( issuer: jwtSettings.Issuer, audience: jwtSettings.Audience, claims: new ListClaim { new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(JwtRegisteredClaimNames.UniqueName, userName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }.Concat(roles.Select(r new Claim(ClaimTypes.Role, r))), expires: DateTime.UtcNow.AddMinutes(jwtSettings.AccessTokenExpirationMinutes), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); }参数说明JwtRegisteredClaimNames.Sub用户唯一标识非用户名建议用数据库IDJwtRegisteredClaimNames.JtiJWT唯一ID用于防重放攻击需存入Redis黑名单expires必须用DateTime.UtcNow禁用DateTime.Now时区陷阱signingCredentials密钥长度必须为32字节HMAC-SHA256要求否则启动时报IDX10634。3. Swagger集成让Bearer Token真正“可点击、可测试、可复现”3.1 Swashbuckle配置不止是加个Authorize按钮很多教程只教AddSwaggerGen里加AddSecurityDefinition却漏掉最关键的AddSecurityRequirement——没有它Swagger根本不会在请求头里自动加Authorization: Bearer xxx。本包Program.cs配置如下// Swagger配置必须放在builder.Build()之后 var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(c { c.SwaggerEndpoint(/swagger/v1/swagger.json, Authentication API v1); c.RoutePrefix swagger; // 避免根路径冲突 // 关键启用Bearer Auth并预填充示例token c.ConfigObject.AdditionalItems[persistAuthorization] true; // 刷新页面保留token c.OAuthAppName(Authentication Service); }); } // 添加JWT认证到Swagger核心 app.UseSwaggerUI(c { c.AddSecurityDefinition(Bearer, new OpenApiSecurityScheme { Name Authorization, Type SecuritySchemeType.Http, Scheme bearer, BearerFormat JWT, In ParameterLocation.Header, Description JWT Authorization header using the Bearer scheme. Example: \Bearer {token}\ }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference new OpenApiReference { Type ReferenceType.SecurityScheme, Id Bearer } }, new string[] { } // 空数组表示该接口需要Bearer认证 } }); });注意AddSecurityRequirement必须在AddSecurityDefinition之后调用且Id Bearer必须与AddSecurityDefinition的Name完全一致大小写敏感。否则Swagger UI显示“Authorize”按钮但点击无反应。3.2 控制器级鉴权[Authorize]的三种用法与血泪经验本包AuthenticationController.cs演示了三种真实场景下的[Authorize]用法场景代码写法作用踩坑点全局强制鉴权[ApiController] [Route(api/[controller])]Controller内所有Action需token若Controller有[AllowAnonymous]的Action如Login必须显式标注否则被父级[Authorize]覆盖角色限定访问[Authorize(Roles Admin,Operator)]仅Admin或Operator角色可访问角色名必须与GenerateAccessToken中ClaimTypes.Role值完全一致区分大小写策略级细粒度控制[Authorize(Policy AtLeastTwoYearsExperience)]需自定义策略见3.3节策略必须在Program.cs中用AddAuthorization注册否则启动时报InvalidOperationException3.3 自定义授权策略用Policy替代硬编码Roles当业务需要“用户等级≥3”或“部门IDHR”等复杂条件时Roles属性力不从心。本包在Program.cs中注册了AtLeastTwoYearsExperience策略// Program.cs 中添加 builder.Services.AddAuthorization(options { options.AddPolicy(AtLeastTwoYearsExperience, policy { policy.RequireAuthenticatedUser(); // 先确保已登录 policy.RequireClaim(YearsOfExperience, 2, 3, 4, 5); // 声明中必须有YearsOfExperience且值为2/3/4/5之一 }); });对应地GenerateAccessToken中需添加该声明new Claim(YearsOfExperience, user.YearsOfExperience.ToString()) // 用户实体中需有YearsOfExperience属性玄学经验RequireClaim的值必须是字符串即使数据库存的是int。若传user.YearsOfExperienceint类型生成的Claim.Value会是2正确但若传user.YearsOfExperience.ToString()显式转string更安全——避免.NET Core序列化时类型推断错误。4. 鉴权链路全排查JWT验签失败的5个高频现场与解法4.1 现象Swagger点Authorize填token后所有接口返回401但日志无报错原因JwtSecurityTokenHandler未注册为Singleton每次验证都新建实例导致TokenValidationParameters未生效。解决在Program.cs中显式注册builder.Services.AddSingletonJwtSecurityTokenHandler();并在JwtTokenService中注入使用。4.2 现象Postman调用Login成功返回token但用该token访问其他接口仍401原因Authorization请求头格式错误。常见错误写成Bearer: xxxxx冒号后多空格写成Bearer xxxxx缺token关键字写成Basic xxxxx混淆Basic Auth解决严格按RFC 7235规范Header值必须为Bearer tokenBearer后一个空格token前无空格。Swagger UI自动处理此格式Postman务必手动检查。4.3 现象本地开发环境token始终验不过部署到Linux服务器反而正常原因Windows系统时间与NTP服务器不同步导致exp过期时间早于当前时间。DateTime.UtcNow在本地机上返回的时间比实际慢。解决Windows下执行w32tm /resync强制同步时间代码中增加容错options.ClockSkew TimeSpan.FromSeconds(30);允许30秒误差生产环境严禁用DateTime.Now一律用DateTime.UtcNow。4.4 现象修改appsettings.Development.json的JwtSettings:Key后旧token仍能通过验证原因JwtSecurityTokenHandler的TokenValidationParameters被缓存未随配置变更实时刷新。解决将TokenValidationParameters封装为Scoped服务在每次验证时重新构建本包JwtTokenService.ValidateToken中实现而非全局静态变量。4.5 现象用户登出后token在过期前仍可访问接口原因JWT是无状态的登出无法主动作废token除非引入Redis黑名单但本包未实现。解决短期方案前端登出时清空localStorage中的token后端不做额外处理长期方案在AuthenticationService中添加RevokeToken方法将jti存入RedisValidateToken时先查黑名单本包实践采用“短时效Refresh Token”组合——Access Token仅30分钟Refresh Token 7天登出时仅删除Refresh Token见AuthenticationController.Logout。5. 进阶实战用Refresh Token实现无缝续签避开JWT最大软肋5.1 为什么必须实现Refresh TokenJWT的“无状态”是把双刃剑服务端不存token省去了数据库查询但代价是——一旦签发无法主动吊销。用户改密码、设备丢失、权限变更时旧token在过期前始终有效。Refresh Token机制正是为解决此问题而生Access Token短时效30分钟Refresh Token长时效7天且Refresh Token存储在服务端数据库/Redis登出时可立即删除。本包AuthenticationController.cs中RefreshTokenAction实现如下[HttpPost(refresh-token)] [AllowAnonymous] public async TaskActionResultAuthResponse RefreshToken([FromBody] RefreshTokenRequest request) { var principal _jwtTokenService.GetPrincipalFromExpiredToken(request.Token); var userId principal.FindFirstValue(JwtRegisteredClaimNames.Sub); // 1. 查询数据库中该用户的Refresh Token是否匹配且未过期 var storedRefreshToken await _context.RefreshTokens .FirstOrDefaultAsync(x x.UserId userId x.Token request.RefreshToken); if (storedRefreshToken null || storedRefreshToken.ExpiresAt DateTime.UtcNow) { return Unauthorized(new AuthResponse { Message Invalid refresh token. }); } // 2. 生成新Access Token var newAccessToken _authenticationService.GenerateAccessToken( userId, principal.FindFirstValue(JwtRegisteredClaimNames.UniqueName), principal.FindAll(ClaimTypes.Role).Select(c c.Value)); // 3. 更新Refresh Token滚动更新 storedRefreshToken.Token Guid.NewGuid().ToString(); storedRefreshToken.ExpiresAt DateTime.UtcNow.AddDays(7); _context.RefreshTokens.Update(storedRefreshToken); await _context.SaveChangesAsync(); return Ok(new AuthResponse { AccessToken newAccessToken, RefreshToken storedRefreshToken.Token, ExpiresIn 30 * 60 // 秒 }); }关键设计点GetPrincipalFromExpiredToken用JwtSecurityTokenHandler的ValidateToken方法解析过期token获取其中的sub用户ID和jtitoken ID避免用户伪造Refresh TokenRefreshToken表结构UserId外键、TokenGUID字符串、ExpiresAtDateTime、CreatedDate审计用滚动更新每次Refresh都生成新Refresh Token旧Token立即失效——这是防重放攻击的核心。5.2 前端调用Refresh Token的容错逻辑SPA项目如Vue/React调用Refresh Token不能简单“失败就跳登录页”。本包配套的Postman集合中Refresh Token请求预设了Tests脚本// Postman Tests 脚本自动续签 if (responseCode.code 200) { // 成功获取新token更新环境变量 pm.environment.set(access_token, jsonData.access_token); pm.environment.set(refresh_token, jsonData.refresh_token); } else if (responseCode.code 401) { // Refresh Token也过期强制登出 pm.environment.unset(access_token); pm.environment.unset(refresh_token); }生产环境必须做前端拦截401响应自动触发Refresh Token请求Refresh失败401时清除所有token并跳转登录页所有API请求前检查Access Token剩余有效期exp-iat 60秒提前刷新。5.3 数据库迁移为Refresh Token建表的EF Core命令本包AuthenticationModel项目中已包含RefreshToken实体类和OnModelCreating配置。执行迁移只需两步# 1. 在AuthenticationModel项目目录下执行 dotnet ef migrations add AddRefreshTokenTable --project AuthenticationModel.csproj --startup-project AuthenticationService.csproj # 2. 更新数据库 dotnet ef database update --project AuthenticationModel.csproj --startup-project AuthenticationService.csproj生成的迁移文件Migrations/{timestamp}_AddRefreshTokenTable.cs中关键配置为modelBuilder.EntityRefreshToken() .HasKey(rt rt.Id); modelBuilder.EntityRefreshToken() .HasIndex(rt rt.UserId); // 为UserId建索引加速查询 modelBuilder.EntityRefreshToken() .HasOne(rt rt.User) .WithMany(u u.RefreshTokens) .HasForeignKey(rt rt.UserId);血泪经验RefreshToken表必须为UserId建索引否则高并发下SELECT * FROM RefreshTokens WHERE UserIdp0 AND Tokenp1会全表扫描QPS超200即拖垮数据库。从那以后我每次加关联表都强制走一遍dotnet ef migrations script看SQL确认索引存在才合代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表