尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

.NET6 WebApi JWT用户鉴权实战:从签发到验证的完整指南

.NET6 WebApi JWT用户鉴权实战:从签发到验证的完整指南 简介基于.NET6平台构建WebApi并集成JWT用户鉴权的完整示例源码面向需要为接口添加安全认证能力的C#后端开发者可摆脱传统Session状态绑定快速实现无状态、可扩展的登录鉴权机制。压缩包共含68个文件以11个C#源文件、14个JSON配置文件和18个DLL依赖库为主体整体仅1.43MB结构紧凑、便于对照学习。项目采用清晰的分层结构涵盖服务层、操作层与数据模型层完整覆盖JWT令牌的生成与验证、Swagger交互文档接入授权配置、[Authorize]特性保护受控接口等核心流程开发者能直观看到从用户登录产生Token到请求头携带Token完成身份校验的全链路实现。目前已有4503人学习下载适合具备一定C#基础、希望快速上手.NET6 WebApi安全开发的读者并可直接扩展角色权限与自定义校验逻辑。1. .NET6 WebApi 用户鉴权JWT 方案没那么玄但坑都在细节里在 .NET6 上做 WebApi第一次被要求“加个登录鉴权”时我第一反应是找现成的 IdentityServer后来发现一个 JWT 方案就够用了。JWTJSON Web Token说白了就是服务端在用户登录成功后签发一张带签名的“通行证”客户端每次请求把它放在 Authorization 头里服务端验签通过就放行。这套方案在 .NET6 WebApi 项目里落地并不复杂只需要处理好多用户登录、签发令牌、配置验证参数、在 Swagger 里带 token 测试这一条链路。适合谁刚接触 .NET 后端安全、想快速给接口加上登录保护的开发者。这份源码把这几步都拆开了正好照着做一遍。2. JWT 鉴权链路从签发到验证的完整逻辑2.1 为什么选 JWT 而不是 Session无状态和扩展性的取舍在 WebApi 场景里鉴权方案无非几种SessionCookie、OAuth、JWT。.NET6 项目里 JWT 之所以常用是因为 WebApi 和传统 MVC 不一样客户端可能是浏览器、小程序、App甚至服务端调用没法保证每个请求都带同一个 Cookie 上下文。JWT 把用户身份信息直接编码到令牌里服务端不存会话状态验签通过就算数。这对水平扩展很友好多个后端实例共享同一个密钥任何一台服务器都能独立验签不需要共享 Session 存储。但也别把 JWT 想成万能钥匙。它最大的问题是 token 一旦签发在过期前无法主动吊销除非你额外维护黑名单。所以 JWT 适合短期有效的接口鉴权不适合做“记住我”式的长期登录也不适合对安全性要求极高的管理后台。我一般会把 JWT 过期时间控制在 12 小时再配合刷新令牌或重新登录这个后面第 6 章会说。2.2 JWT 的结构Header、Payload、Signature 三段怎么拼一个 JWT 长这样eyJhbGciOi...分三段用点号分隔。第一段 Header 声明签名算法和令牌类型第二段 Payload 放自定义声明比如用户 ID、角色、过期时间第三段 Signature 用密钥对前两段做签名。Payload 里的信息是 Base64Url 编码的不是加密的任何拿到 token 的人都能解出来看所以千万别把密码等敏感信息写进 Payload。在 .NET6 里生成和解析 JWT 用的是System.IdentityModel.Tokens.Jwt这个 NuGet 包配合Microsoft.IdentityModel.Tokens。前者负责把 Claims 拼成 JWT 字符串后者提供签名算法、密钥对象和验证参数。这两个包是微软官方维护的版本跟着 .NET 6 走直接用就好。2.3 签发和验签的关键参数先看签发端。JwtSecurityTokenHandler类的CreateToken方法需要一个SecurityTokenDescriptor里面要设置 Issuer签发者、Audience受众通常填 WebApi 项目名、Expires过期时间、SigningCredentials签名凭据。签名凭据用SymmetricSecurityKey包一层算法用 HmacSha256这是 JWT 最常见的对称签名方式签发和验证用同一个密钥。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: new ListClaim { new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Role, user.Role) }, expires: DateTime.UtcNow.AddHours(2), signingCredentials: credentials ); var tokenString new JwtSecurityTokenHandler().WriteToken(token);逻辑说明这里先构造对称密钥和签名凭据然后把用户 ID 和角色放到 Claims 里过期时间设成 2 小时后最后用WriteToken序列化成字符串返回给客户端。注意 Claims 里的 NameIdentifier 对应[Authorize]之后的User.FindFirst(ClaimTypes.NameIdentifier)读取角色声明则会被 .NET 自动映射到IsInRole判断。参数说明Jwt:Key、Jwt:Issuer、Jwt:Audience三个配置项分别对应签名密钥、签发者和受众过期时间用 UTC 时间避免本地时间和服务器时间偏差导致提前过期或长期不过期。验证端代码services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer config[Jwt:Issuer], ValidateAudience true, ValidAudience config[Jwt:Audience], ValidateLifetime true, ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(config[Jwt:Key])) }; });这段注册的是 JwtBearer 认证中间件整个请求管线在控制器执行前会先检查 Authorization 头取出 Bearer 后面的 token 做验签。ValidateLifetime开启后过期 token 会被拒绝并返回 401。这里有个细节IssuerSigningKey必须和签发时的 key 完全一致少一个字符都不行最常见的翻车就是把 appsettings.json 里的密钥换掉之后忘了重新生成 token。3. 登录接口与 [Authorize] 拦截在 .NET6 里走通完整流程3.1 项目结构与分层AuthenticationService 里的三个工程这份源码的解决方案文件名是AuthenticationService.sln里面实际上拆了三个工程AuthenticationModel、AuthenticationOperation、AuthenticationService。AuthenticationModel放数据模型比如用户实体、登录请求 DTO、Token 响应 DTOAuthenticationOperation放业务操作方法比如CheckUser、CreateToken这类AuthenticationService是 WebApi 主工程包含 Controllers、Program.cs、appsettings.json。这种拆法不是必须的但好处是 Model 和 Operation 可以被多个项目复用比如以后要加一个管理后台或者控制台程序去调同一个登录逻辑直接引这两个工程就行。3.2 登录接口验证用户并签发 Token登录接口一般长这样[HttpPost(login)] public async TaskIActionResult Login([FromBody] LoginRequest request) { var user await _authenticationOperation.FindUser(request.UserName); if (user null || user.Password ! request.Password) return Unauthorized(new { message 用户名或密码错误 }); var token _authenticationOperation.CreateToken(user); return Ok(new { token token, expiresIn 7200 }); }逻辑说明先按用户名查用户比对密码。这里源码里是直接比较明文密码的因为在示例项目里是为了演示 JWT 流程真实的项目需要换成哈希存储和验证比如 BCrypt 或 PBKDF2。密码验证通过后调用CreateToken把用户 ID 和角色塞进 Claims 签发 token返回格式里带上过期时间方便前端控制续期逻辑。参数说明Unauthorized返回 401 而不是 200这是为了让前端能直接通过状态码跳转到登录页expiresIn单位是秒前端通常拿它做 token 过期前的刷新提示。3.3 受保护接口[Authorize] 和 [AllowAnonymous]控制器上标记[Authorize]之后这块就没有“匿名访问”了。比如WeatherForecastController在模板里是公开的加上[Authorize]后不带 token 访问会得到 401。但如果你有的接口想放开比如注册、验证码就用[AllowAnonymous]覆盖。这里要分清两个特性的层级[Authorize]可以挂在 Controller 类上也可以挂在 Action 方法上。挂在类上意味着整个控制器的所有接口都要登录挂在方法上是单独控制。这俩混用时方法上的[AllowAnonymous]优先级高于类上的[Authorize]实际开发里我习惯把“全局要登录”写成一个约定再在个别放开的接口上用[AllowAnonymous]标注这样比每个方法都加[Authorize]更不容易漏。程序里还可以用FallbackPolicy做全局兜底在 Program.cs 里加一句builder.Services.AddAuthorization(options { options.FallbackPolicy new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); });这句话的意思是所有没显式标注[Authorize]或[AllowAnonymous]的接口默认都要求已认证。有了这个兜底策略就不会出现“忘了加标签导致接口裸奔”的情况。3.4 从 Token 里取用户信息Claims 的读取方式[Authorize] 验证通过后控制器里怎么拿到当前用户两个方式。一个是直接用HttpContext.User这是认证中间件填充好的ClaimsPrincipal里面有你在签发时放进去的 Claims比如User.Identity?.Name是 NameIdentifierUser.IsInRole(admin)是角色判断。另一个是读HttpContext.User.FindFirst(UserId)?.Value适合自定义的 claim 类型。我一般会在 controller 里加一个私有属性来收敛这段读取逻辑避免每个 action 里都写 FindFirst。比如private string CurrentUserId User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;这样接口里需要当前用户 ID 的地方直接引用CurrentUserId而不用到处写 FindFirst 的样板代码。4. Swagger 里配置 JWT 认证带 Token 测试接口的正确姿势4.1 Swagger UI 的认证按钮是怎么出来的Swagger 默认只提供接口列表和参数表单不会自动出现锁形图标。要让 UI 上出现 Authorize 按钮需要在AddSwaggerGen的 options 里注册 SecurityScheme 和 SecurityRequirement。builder.Services.AddSwaggerGen(options { options.AddSecurityDefinition(Bearer, new OpenApiSecurityScheme { Name Authorization, Type SecuritySchemeType.Http, Scheme Bearer, BearerFormat JWT, In ParameterLocation.Header, Description 请输入 token不要带 Bearer 前缀 }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference new OpenApiReference { Type ReferenceType.SecurityScheme, Id Bearer } }, Array.Emptystring() } }); });这段配置看起来啰嗦但每个属性都对。SecuritySchemeType.Http加SchemeBearerSwagger UI 才会在请求头里拼出Authorization: Bearer token这样的格式。InHeader指定了 token 放在请求头而不是 URL 或 Cookie。Description 里我习惯写“不要带 Bearer 前缀”因为 UI 输入框里如果已经自动带上了 Bearer拼出来会变成Bearer Bearer xxx这种翻车格式。4.2 UseSwagger 和 UseSwaggerUI 的顺序Program.cs 中间件顺序在 .NET 6 里是个重点。UseSwagger放在UseAuthorization前面、UseRouting后面这算是常规写法UseSwaggerUI放在UseEndpoints之前。不过 .NET 6 的 WebApplication 模板里UseAuthorization后面就是MapControllersSwagger 中间件加在哪其实只影响文档路径能否被访问不影响业务接口本身。一个比较容易踩的坑如果你把UseSwaggerUI放在了MapControllers之后Swagger UI 页面虽然能打开但里面的接口没法直接调试。因为 Swagger 自带的重定向和静态资源处理是按中间件顺序执行的位置靠后会导致 CSS/JS 资源加载不出来。我就踩过这个本地调试一切正常发布到服务器后用域名访问 Swagger发现页面是白的控制台一堆 404最后发现问题出在把所有静态文件请求交给后端路由处理了Swagger 的静态资源根本没被中间件拦截到。解决办法是在UseRouting之前调用UseStaticFiles或者把UseSwaggerUI放在MapControllers之前。4.3 发布后 Swagger 404 排查与 JWT 发包格式发布 WebApi 项目后提示not found /swagger/v1/swagger.json这个问题在 .NET6 里很典型。Swagger 404 一般是两个原因一是 App 环境不是 Development而 Swagger 被条件编译拦掉了比如在 Program.cs 里写了if (env.IsDevelopment())二是相对路径问题发布到二级路径域名后面带子目录时Swagger 默认的相对路径找不到 swagger.json。对策把 Swagger 的开关改成配置控制生产环境想开就开if (builder.Environment.IsDevelopment() || config[Swagger:Enabled] true) { app.UseSwagger(); app.UseSwaggerUI(); }二级路径就设置UseSwaggerUI的RoutePrefixapp.UseSwaggerUI(options { options.SwaggerEndpoint(/swagger/v1/swagger.json, AuthService V1); });这里的前导斜杠要特别注意如果是部署在域名子目录下SwaggerEndpoint里应该带上完整的虚拟路径不然 UI 能打开但 json 请求 404。这两个调整是发布 .NET6 WebApi 时最常做的事。测试时用 curl 直接验证 JWT 发包格式也很有用。带 token 的请求是这样curl -X GET http://localhost:5000/WeatherForecast \ -H Authorization: Bearer eyJhbGciOi...注意Bearer和 token 之间是一个空格headers 名称不能写错。Postman 或者 Swagger UI 里只要配好 Authorization 头就能复现同样的效果而 curl 适合在服务器上快速验证发布后的接口是否有鉴权问题。5. JWT 鉴权常见问题与避坑记录五条排错经验5.1 现象返回 401 但日志里没有异常信息这可能是因为 token 过期了。JwtBearer 中间件验签失败时默认不会把错误写到响应体里只有在日志级别调到 Debug 才能看到 “Token validation failed” 之类的记录。我排查的第一步是先看响应头里有没有WWW-Authenticate: Bearer errorinvalid_token有就说明是 token 本身的问题。解决给中间件的OnChallenge事件加一个result.HandleResponse()然后自己写 401 的 message 内容这样前端能看到具体失败原因。options.Events new JwtBearerEvents { OnChallenge context { context.HandleResponse(); context.Response.StatusCode 401; context.Response.ContentType application/json; return context.Response.WriteAsJsonAsync( new { code 401, message token 无效或已过期 }); } };5.2 现象明明配置一致却报 SecurityTokenSignatureKeyNotFoundException这个异常名字很长直观说就是验签密钥对不上。原因往往是注册中间件和签发 token 时用了不同的配置来源比如签发时直接写死在代码里验证时又读 appsettings.json或者两个环境配置不同步。解决把密钥收敛到一个地方签发验证都通过 IOptions 读取同一个配置对象。另外密钥的编码也有讲究.NET 里UTF8.GetBytes和ASCII.GetBytes对中文字符的处理不同密钥一旦包含中文或特殊字符两边解析结果不一致就会验签失败。我建议密钥用纯 ASCII 的随机字符串长度 32 字节以上放在 appsettings 里用同一套编码读取。5.3 现象Swagger 里点 Authorize 输入 token 后请求头还是不带原因没有按 4.1 那样完整注册 SecurityScheme或注册时In设成了 Header 但Type设成了ApiKey。ApiKey类型会让 Swagger UI 认为 token 应该放在X-API-Key这种自定义头里而不是标准的 Authorization。解决按照第 4 章的配置Type用HttpScheme用bearer。5.4 现象同样的 token有时能过有时不能过大概率是多个 WebApi 实例部署时不共享时钟或密钥。比如容器化部署里两个实例的 UTC 时钟差了几十秒而 token 的签发时间正好在边界上导致个别实例判过期。解决把TokenValidationParameters里的ClockSkew放宽一点默认是 5 分钟如果没生效就确认 NTP 同步密钥必须完全一致这不用多说了。options.TokenValidationParameters new TokenValidationParameters { ValidateLifetime true, ClockSkew TimeSpan.FromMinutes(2) };5.5 现象需要角色权限但 [Authorize(Roles admin)] 一直 403[Authorize(Roles admin)] 就可以做角色鉴权。注意两个前提签发 token 时 Claims 里必须存在角色声明角色值必须与 Claims 里的值完全匹配大小写敏感。常见的坑是在 Payload 里自定义了一个叫role的 claim但[Authorize(Roles ...)]读的是微软标准的 role claim type两者对不上。解决签发时用ClaimTypes.Role不要用自定义字符串。6. 进阶技巧关闭 Claim 映射与鉴权验证三板斧6.1 关闭 Claim 自动映射让角色认证不再“失灵”JwtSecurityTokenHandler默认会把 JWT 里的 short claim 名映射到微软的 long URI 格式所以你自定义的Role在验证后可能变成http://schemas.microsoft.com/ws/2008/06/identity/claims/role导致你按原名找不到。碰到[Authorize(Roles)]不生效时先检查这个。解决办法是在TokenValidationParameters里关掉映射并指定角色声明类型options.TokenValidationParameters new TokenValidationParameters { MapInboundClaims false, RoleClaimType Role };逻辑说明MapInboundClaims false会让 JWT 里的 claim 名原样保留Role就还是RoleRoleClaimType告诉授权中间件去哪找角色列表。这两个属性配合起来[Authorize(Roles admin)]才能按你签发的原始角色名生效。第一次搭 JWT 鉴权时最容易在这一步耗上半天其实就是在和默认映射较劲。6.2 验证鉴权是否生效的三种方式第一种是看 Swagger UI 里的锁形图标输入 token 后每个接口都能正常 200第二种是用 curl 走一遍不带 token 和带 token 的请求curl -i http://localhost:5000/WeatherForecast curl -i -H Authorization: Bearer token http://localhost:5000/WeatherForecast不带 token 返回 401带 token 返回 200链路就算通了。第三种是临时加OnTokenValidated打印 claimsoptions.Events new JwtBearerEvents { OnTokenValidated context { var claims context.Principal?.Claims .Select(c ${c.Type}: {c.Value}); Console.WriteLine(string.Join(, , claims ?? new Liststring())); return Task.CompletedTask; } };这样每个通过验证的请求都会在控制台打印一行声明清单对照签发端写的 Claims一眼就能看出是名被映射改了还是值不匹配。从那以后我每次在 .NET6 WebApi 项目里接 JWT都会强制自己完成三件事把签发和验证的密钥配置收敛到同一个来源不再写两份调角色鉴权前先打印一遍 claims确认类型没被默认映射改掉最后用 curl 不带 token 和带 token 各打一遍确认 401 和 200 都符合预期。这三件事加起来不到五分钟但每次都能省下半小时的排错时间希望帮到你。本文还有配套的精品资源点击获取
返回列表