简介:这是一份基于.NET6平台构建WebApi并集成JWT用户鉴权与Swagger测试的完整示例源码,面向正在学习C# WebApi开发、需要落地前后端分离鉴权流程的中级开发者。资源将用户登录、令牌生成与验证、接口授权及Swagger联调整合在同一个解决方案中,可以直接运行并对照理解。压缩包共68个文件,以dll、json、cs、cache等类型为主,其中cs源码对应控制器、服务与模型分层,json含配置文件与依赖信息,dll为项目构建所引用的程序集,整体大小仅1.43MB,方便快速下载与部署。目前已有4503人学习或下载。通过这套工程,读者能掌握JwtSecurityTokenHandler生成令牌、在Authorization头传递JWT、配合[Authorize]保护接口,以及为Swagger配置JWT鉴权按钮等关键技能,可作为团队开发或毕业设计中的用户权限基础模块,也可继续扩展角色管理、刷新令牌等功能。
1. 从登录接口到JWT鉴权:这份.NET6 WebApi资源解决了什么问题
联调第一天,前端把token放Authorization头里发过来,后端接口却直接401——不是token过期,而是整条JWT校验管道没接上,签发的token没人验。这个现象在新手项目里出现的频率高得吓人。这份源码包给出的正是.NET6平台上一整套能直接跑起来的WebApi用户鉴权方案:三层项目结构(Model / Operation / Service)、登录接口颁发JWT、Swagger里一键携带token调试受保护接口。和网上只贴两段代码的教程不同,它把Controller、业务操作类、模型、配置全部分层放好,适合刚接触C# WebApi、想弄懂用户鉴权完整链路的新手,也适合想给没做鉴权的老项目快速接入JWT的开发者。
2. 拆解AuthenticationService.sln:三层项目结构与JWT运行链路
打开源码包,先看AuthenticationService.sln。一个解决方案文件对应三个项目:AuthenticationModel、AuthenticationOperation、AuthenticationService。这也是很多.NET服务端项目的常见分层:数据模型放一层、业务操作放一层、Web宿主放一层。这种拆分不是摆设,它解决的是复用和职责边界的问题——同一个token生成逻辑放到Operation类库里,以后再做管理后台、再造一个调用端,直接引用类库就可以,不需要把Controller那一层也搬过去。
2.1 三个项目各管什么:Model、Operation、Service的职责边界
文件列表里能明显看到的几个关键入口:AuthenticationService下有Controllers、Program.cs、appsettings.json;AuthenticationOperation下有AuthenticationOperation.cs、OperationClass、Utility;AuthenticationModel下有AuthenticationModel.cs。按我拆项目的习惯,先看Controllers和Program.cs,因为它们是入口;再看AuthenticationOperation里的JwtTokenOperation,这是整个鉴权的核心逻辑;最后补AuthenticationModel里的实体定义。
AuthenticationService.sln ├── AuthenticationService/ // WebApi宿主,接收HTTP请求 │ ├── Controllers/ │ │ ├── AuthenticationController.cs │ │ └── WeatherForecastController.cs │ ├── Program.cs │ └── appsettings.json ├── AuthenticationOperation/ // 业务操作类,生成Token的核心逻辑 │ ├── AuthenticationOperation.cs │ ├── OperationClass/ │ └── Utility/ └── AuthenticationModel/ // 数据模型,身份验证相关实体 └── AuthenticationModel.cs这个目录结构对应的依赖关系是:AuthenticationService引用AuthenticationOperation,AuthenticationOperation引用AuthenticationModel。所以Controller里只写HTTP请求相关的动作,token生成细节在Operation层,实体定义回Model。新手最容易犯的错是把UserInfo模型直接写在Controller文件里,短期看着方便,一旦要加角色管理、权限扩展,就到处找类。分成三层之后,扩展点非常清晰:加字段去Model,加逻辑去Operation,加接口去Controller。
有一点要提前说明:AuthenticationOperation类库要读到appsettings.json里的JwtSettings,需要引用Microsoft.Extensions.Configuration.Abstractions这个包。源码包编译时如果提示缺依赖,在类库上右键“管理NuGet程序包”装一下就好,不需要改任何代码。
2.2 JWT令牌的三段结构:Header、Payload、Signature
JWT不是一串无意义的乱码,由Header、Payload、Signature三段Base64Url编码拼接而成,各段用点号隔开。一个典型的JWT长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE3MzAwMDAwMDAsImlhdCI6MTczMDAwMDAwMH0 . xgEYdHgfH8bPcT9pY7yIzCzJ6sTMO3zY_KXjD9TJG4s第一段Header声明了加密算法和令牌类型,这里alg是HS256,typ是JWT。第二段Payload存放claims,也就是登录用户是谁、角色是什么、过期时间exp、签发时间iat这些信息。第三段Signature是签名,做法是拿前两段拼接的字符串,用HMACSHA256和密钥算出来的哈希值。
签名是最关键的部分。Header和Payload是Base64Url编码,不是加密,任何人都能解码看内容;但第三段签名只有持有密钥的服务端能生成和校验。客户端如果偷偷把用户名从admin改成guest,签名会立刻失效,因为重新算出的哈希对不上。这个机制决定了JWT适合做无状态鉴权:服务端不存session,拿到token验一下签名就够了。
2.3 完整请求链路:登录、携带、校验
把整条链路串起来看是这样一个过程:客户端先调登录接口,服务端校验账号密码后生成JWT返回;客户端把token存到localStorage或内存里;后续每次请求,在Header带Authorization: Bearer {token}。服务端由认证中间件解析并校验签名、过期时间、签发者等信息,校验通过就把token里的claims转成当前用户身份,[Authorize]接口才放行。这套流程在AuthenticationService里对应三段代码:JwtTokenOperation类负责生成token,Program.cs里AddJwtBearer负责配置校验参数,AuthenticationController负责登录入口和受保护接口。
我习惯把这个链路理解成两次握手的组合:第一次是账号密码换token,第二次是token换用户身份。第一握手里token要不要包含角色信息,直接决定了第二握手时接口能不能区分普通用户和管理员。比如后面要做的[Authorize(Roles = "Admin")]这类限制,依赖的就是签发token时把角色写进了claims。
3. 登录接口与Token生成:把核心代码跑通
原理放在前面,是为了让代码有落脚的依据。接下来直接动手,从建项目到调通登录接口,按我平时搭WebApi的顺序一步步来。整套源码的核心代码集中在三处:appsettings.json的JwtSettings节点、Program.cs里的认证注册、AuthenticationOperation里的JwtTokenOperation。先把这三处写清楚,登录接口就顺理成章了。
3.1 安装NuGet包与配置appsettings.json
要在.NET6 WebApi里跑通JWT,需要先安装四个包:System.IdentityModel.Tokens.Jwt、Microsoft.IdentityModel.Tokens、Microsoft.AspNetCore.Authentication.JwtBearer、Swashbuckle.AspNetCore。前两个负责生成和解析JWT,第三个是认证中间件,第四个是Swagger文档。源码包里已经处理好了包引用,这里列出是为了让照着手搭项目的读者知道依赖从哪来。
{ "JwtSettings": { "Issuer": "AuthenticationService", "Audience": "AuthenticationApiClient", "SecretKey": "YourSuperSecretKeyForJwtSigningAtLeast32BytesLongValue", "ExpiresMinutes": 120 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }JwtSettings这四个参数是整套鉴权的基础配置,Issuer表示签发者,Audience表示接受方,SecretKey是签名密钥,ExpiresMinutes控制token有效期。实际项目中,我会把SecretKey放到用户机密或环境变量里,避免明文提交到仓库。密钥还有一个硬性要求:HS256签名算法要求SecretKey换算成字节后不少于32字节,所以字符串长度最好控制在32个字符以上,并且用随机字符串,不要用“123456”这种。
| 参数 | 示例值 | 作用 | 注意点 |
|---|---|---|---|
| Issuer | AuthenticationService | 签发者标识 | 与TokenValidationParameters.ValidIssuer保持一致 |
| Audience | AuthenticationApiClient | 受众标识 | 与ValidAudience保持一致 |
| SecretKey | 32字节以上随机字符串 | 签名密钥 | 密钥泄露等于任何人都能伪造token |
| ExpiresMinutes | 120 | token过期时间 | 真实项目建议15-30分钟并配合刷新 |
3.2 Program.cs注册认证服务与TokenValidationParameters
Program.cs是.NET6 WebApi程序的入口,认证服务的注册和中间件的挂载都发生在这里。这段配置如果你照着网上的老教程抄,很容易漏掉AddAuthentication那一段,直接写AddJwtBearer,结果就是后面调接口时认证方案没有默认值。
using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using AuthenticationOperation; var builder = WebApplication.CreateBuilder(args); var jwtSettings = builder.Configuration.GetSection("JwtSettings"); builder.Services.AddScoped<JwtTokenOperation>(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtSettings["Issuer"], ValidateAudience = true, ValidAudience = jwtSettings["Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]) ), ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1) }; }); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();这段代码做的是三件事:注册Service层依赖、配置认证方案、挂载中间件。TokenValidationParameters里的几个Validate开关分别控制是否校验签发者、受众、签名密钥和过期时间,实际使用中建议全部打开。ClockSkew默认值是5分钟,意思是允许token过期后5分钟内的宽容期,如果你希望到点立即失效,就把值设小一点。我一般设1分钟,既避免客户端和服务器时间误差导致的误伤,又不至于让过期token在5分钟内还能用。
这里有个顺序坑提前说:app.UseAuthentication()必须在app.UseAuthorization()之前调用。如果顺序写反,认证中间件还没执行,授权中间件拿不到用户身份,[Authorize]接口会全部失守。
3.3 JwtTokenOperation:Token生成的核心实现
JwtTokenOperation放在AuthenticationOperation层,专门负责生成JWT。这个类只做一件事:接收一个用户实体,把用户信息塞进claims,然后返回签好的token。
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; using AuthenticationModel; namespace AuthenticationOperation { public class JwtTokenOperation { private readonly IConfiguration _configuration; public JwtTokenOperation(IConfiguration configuration) { _configuration = configuration; } public string GenerateToken(UserInfo user) { var secretKey = _configuration["JwtSettings:SecretKey"]; var issuer = _configuration["JwtSettings:Issuer"]; var audience = _configuration["JwtSettings:Audience"]; var expiresMinutes = Convert.ToInt32(_configuration["JwtSettings:ExpiresMinutes"]); var claims = new[] { new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Role, user.Role) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: issuer, audience: audience, claims: claims, notBefore: DateTime.UtcNow, expires: DateTime.UtcNow.AddMinutes(expiresMinutes), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } } }生成token的过程可以拆成五步:声明claims、创建对称密钥、创建签名凭据、构造JwtSecurityToken、用Handler序列化成字符串。claims里的ClaimTypes.Name、ClaimTypes.NameIdentifier、ClaimTypes.Role分别对应用户显示名、用户ID和角色,这三条在后面接口鉴权时会被频繁读取。notBefore和expires我统一用DateTime.UtcNow,避免本地时间与UTC时间差导致token提前过期。JwtSecurityTokenHandler().WriteToken最终把对象转成客户端看到的那个三段落字符串。
3.4 AuthenticationController:登录入口与用户信息读取
Controller这一层只做HTTP动作,登录校验逻辑在这份源码里是硬编码的admin/123456,真实项目替换成查数据库即可。重点看token如何从生成到返回,以及受保护接口如何用[Authorize]挡人。
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using AuthenticationModel; using AuthenticationOperation; namespace AuthenticationService.Controllers { [ApiController] [Route("api/[controller]")] public class AuthenticationController : ControllerBase { private readonly JwtTokenOperation _tokenOperation; public AuthenticationController(JwtTokenOperation tokenOperation) { _tokenOperation = tokenOperation; } [HttpPost("login")] [AllowAnonymous] public IActionResult Login([FromBody] LoginRequest request) { // 示例项目没有接数据库,先写死账号密码,真实项目改为查数据表 if (request.UserName == "admin" && request.Password == "123456") { var user = new UserInfo { Id = 1, UserName = request.UserName, Role = "Admin" }; var token = _tokenOperation.GenerateToken(user); return Ok(new LoginResult { Success = true, Token = token, Message = "登录成功" }); } return Unauthorized(new LoginResult { Success = false, Token = "", Message = "用户名或密码错误" }); } [HttpGet("userinfo")] [Authorize] public IActionResult GetUserInfo() { var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; var userName = User.Identity.Name; var role = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; return Ok(new { UserId = userId, UserName = userName, Role = role, Message = "token有效,接口访问成功" }); } } }[AllowAnonymous]放在Login上,表示这个接口不需要token就能访问;[Authorize]放在GetUserInfo上,表示必须携带有效token。User.Identity和User.Claims是认证中间件解析token后填充的用户身份,取出来的就是签发时写进去的claims。有一点要注意:User.Identity.Name能不能取到值,取决于签发时是否用了ClaimTypes.Name。如果随便写个字符串“UserName”,这里Identity.Name就是空,后面第5章会专门讲这个坑。
4. Swagger配置与接口鉴权:让调试工具直接带上token
做WebApi开发,Swagger基本成了标配。它不只是看接口文档的地方,还能直接调试接口。但默认情况下Swagger不会自动携带token,需要手动配置安全方案。如果在Swagger页面上点开接口直接执行,不带token的请求必然401。所以这一步要解决两件事:让Swagger显示出Authorize按钮,让用户填了token之后所有调试请求都自动带上Authorization头。
4.1 给Swagger挂上Authorize按钮
Swagger的JWT配置依赖Swashbuckle.AspNetCore包。核心动作是在AddSwaggerGen里调用AddSecurityDefinition和AddSecurityRequirement,把JWT的Bearer认证方案声明进去。
builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "AuthenticationService API", Version = "v1" }); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "Bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "请输入JWT令牌,格式:Bearer {token}" }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty<string>() } }); });这段配置里最关键的是SecuritySchemeType.Http加Scheme=Bearer的组合。它告诉Swagger:这是一个HTTP Bearer认证,Swagger UI会渲染成一个输入框,执行调试时自动把Authorization头拼成“Bearer {你输入的内容}”。前阵子按老教程配置的人喜欢用SecuritySchemeType.ApiKey加In=Header,那样Swagger虽然也能显示输入框,但不会自动加Bearer前缀,需要手动把Bearer三个字一起填进去,容易踩第5章的坑。SecurityRequirement那段是把刚才定义好的方案挂到所有接口上,如果没有这段,界面右上角不会出现Authorize按钮,或者出现了也不生效。
Program.cs里还需要启用Swagger中间件,一般放在开发环境的判断里:
if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }正式发布WebApi项目时,记得把这段包在IsDevelopment()里,或者用条件编译控制,别让生产环境暴露Swagger文档。这一步经常被忽略,接口文档一旦开放出去,等于给攻击者递了地图。
4.2 [Authorize]与[AllowAnonymous]:哪些接口需要token
鉴权的最小单位是Controller里的Action。把[Authorize]挂在Controller类上,表示整个控制器的所有接口都需要token;挂在具体Action上,则只保护那一个接口。对应的[AllowAnonymous]用来放行公开接口。这套源码里的AuthenticationController就是典型的组合用法:整个类的接口都要求登录,但Login接口用[AllowAnonymous]放行,毕竟登录时还没有token。
从设计角度讲,我一般建议全站默认鉴权,只对登录、注册、验证码这类接口开放。做法是给Controller统一加[Authorize],再在个别Action上叠加[AllowAnonymous]。如果项目里Controller很多,也可以直接在Program.cs里配置FallbackPolicy,实现所有没有显式标注的接口默认都要求认证。这一点是很多新手忽略的:光在Swagger里看到接口列表,不代表所有接口都被保护了。
4.3 从Claims读取用户身份信息
接口鉴权通过后,业务代码里拿用户身份的方式是通过Controller基类的User属性。User的类型是ClaimsPrincipal,它由认证中间件在解析token时构造出来,token里写进去的claims会映射到User.Claims集合中。读起来就是遍历用户Claims的过程。
[HttpGet("userinfo")] [Authorize] public IActionResult GetUserInfo() { var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; var userName = User.Identity.Name; var role = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; return Ok(new { UserId = userId, UserName = userName, Role = role, Message = "token有效,接口访问成功" }); }这段代码演示了两个读取路径:User.Identity.Name直接取当前登录名,对应签发时的ClaimTypes.Name;User.Claims按Type取值,适合取ID、角色等非姓名信息。取出来的值就是签发token时写进JWT的claim内容,所以前面强调过,一个项目里claims的命名必须前后一致,否则这边读出来就是null。真正的线上项目里,通常还会有DepartmentId、TenantId这类自定义claim,读取逻辑跟这里完全一样,只是Type字符串要自定义。建议把这层读取封装成一个扩展方法,比如GetUserId(this ClaimsPrincipal user),避免每个Controller都写一遍FirstOrDefault。
5. 常见问题排查:五个绕不开的JWT实操坑
这部分是我在一线对接这些项目时最常看到的踩坑记录。前面代码写得很顺,一到联调就集体翻车,而且翻车方式高度雷同。网上那些JWT漏洞总结,第一屏基本都在讲密钥泄露、算法混淆、claims信任,但落到实际项目里,反而不是这些高深问题,是下面五个基础动作没做到位。我把它们按“现象、原因、解决”的格式写在这里,每条都能直接对照自查。
5.1 Swagger弹窗里填了token,接口还是401
现象:登录接口正常返回token,在Swagger右上角Authorize里填入token并保存,调userinfo接口还是401。
原因:安全方案定义为Http+Scheme=Bearer后,Swagger会自动在输入框内容前拼“Bearer ”。如果你把“Bearer eyJhbGciOi...”整段粘进去,实际请求头变成“Bearer Bearer eyJhbGci...”,认证中间件解析失败。
解决:在Swagger的Authorize弹窗里只粘贴JWT本体,不带Bearer前缀。粘完确认一下粘贴框里token是不是以字母e开头,如果以B开头说明Bearer前缀重复了。这个坑我见的次数最多,基本每次给同事调Swagger都能撞上。
5.2 签名验证失败:SecretKey长度与编码坑
现象:登录成功后拿token请求接口,报IDX10503或签名不匹配,签发的令牌无法通过签名校验。从JWT漏洞角度看,这属于密钥配置不当导致的可用性问题,虽然没有泄露,但会让整个服务不可用。
原因:一是SecretKey长度不足,HS256要求32字节,用了“test123”这种太短的字符串;二是appsettings.json里字符串前后带了不可见空格,或者编辑器把反斜杠转义了,导致生成和验证用的key不一致。
解决:先量一下密钥长度,直接在Program.cs里临时加一行日志输出Encoding.UTF8.GetBytes(secretKey).Length,确认不小于32。然后检查appsettings.json里SecretKey那一行有没有多余空格,建议把字符串复制到支持显示空白字符的编辑器里看一遍。密钥生成我一般用openssl rand -base64 48,一把梭取回来直接用。
5.3 token刚返回就提示过期
现象:ExpiresMinutes配了120,登录接口也返回了token,但请求一次就被提示token过期,或者压根走不到业务代码。
原因:时间基准不统一。签发时用了DateTime.Now,而JWT内部统一用Unix时间戳表示UTC时间,校验时按UTC解析;如果你的服务器系统时区不是UTC,DateTime.Now与UTC之间差出的几个小时就可能让token看起来已经失效。另一个常见原因是ClockSkew被设成TimeSpan.Zero,且客户端机器时间快了几分钟,严格校验就触发过期。
解决:所有时间字段统一用DateTime.UtcNow来设置notBefore和expires。ClockSkew不要设置成0,保留1-5分钟容差。部署到海外服务器时,尤其注意服务器时间的时区设置,直接在服务器上执行date -u确认UTC时间是否准确。这一条在多人协作时特别容易翻车,因为每个人本地时间不一样,跑出来的结果也不一样,最后互相甩锅。
5.4 [Authorize]不生效:UseAuthentication顺序问题
现象:给接口加了[Authorize],但不带token也能直接访问,后端完全不拦。
原因:Program.cs里没有调用app.UseAuthentication(),或者把UseAuthentication写在了UseAuthorization后面。认证中间件没执行,授权中间件拿不到任何用户身份,[Authorize]就不会触发401挑战。
解决:在app.Build()之后,先UseAuthentication,再UseAuthorization,这是固定顺序。检查代码时直接看这两行顺序,前者一旦缺失,所有[Authorize]接口全部失守。这一条是影响面最大的坑,线上出问题往往不是某个人改错,而是几个人接手时把中间件顺序调换过。
5.5 Claims读取为空:类型映射不一致
现象:接口已经通过鉴权,但User.Identity.Name为空,或者Role等自定义claim取值是null。
原因:签发token时用了一个自定义字符串“role”当claim类型,而读取时用ClaimTypes.Role;或者反过来,写入用ClaimTypes.Role,读取用JwtSecurityTokenHandler.DefaultInboundClaimTypeMap映射后的短名,导致键对不上。JWT的claim类型通常是一长串URI,而JwtSecurityTokenHandler默认会把一部分标准类型映射成短名,比如ClaimTypes.Role映射成“role”。
解决:最省心的方法是写入和读取都直接用ClaimTypes.*常量,不自行发明字符串。如果非要自定义claim名,就在JwtBearer配置里设置options.MapInboundClaims = false,关闭默认映射,然后读写都用自己的类型字符串。这个选项很多人不知道,排查的时候像黑匣子一样,其实就是一个开关的事。
6. 上线前验证:curl实测与token续签思路
6.1 用curl跑通登录和受保护接口
源码包里的Swagger已经能完成大部分调试,但自动化测试或写联调脚本时,curl更直接。可以先用curl拿到token,再带着token访问受保护接口。
# 第一步:调用登录接口获取token curl -X POST "http://localhost:5000/api/Authentication/login" \ -H "Content-Type: application/json" \ -d '{"userName":"admin","password":"123456"}' # 第二步:把上一步返回的token粘贴到下面,再请求受保护接口 curl -X GET "http://localhost:5000/api/Authentication/userinfo" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."用curl做验证要做到两点:第一步的响应里能拿到token字段,第二步的响应码是200且body里出现用户信息。如果第二步是401,直接按第5章顺序排查看是哪种坑。注意如果token返回到了JSON里,直接复制会带引号导致发送失败,可以用python或jq做提取。
6.2 token续签思路与双token方案
JWT是一次性无状态凭证,一旦过期就只能重新登录。这在移动端场景里很烦人,所以实际项目里普遍用双token方案:access token有效期30分钟,refresh token有效期7天。refresh token不走JWT签名,而是存到Redis或数据库,附带随机字符串,请求刷新接口时用它换新的access token。
[HttpPost("refresh")] [AllowAnonymous] public IActionResult Refresh([FromBody] RefreshRequest request) { // 1. 校验refresh token在Redis中存在且未过期 // 2. 存在则调用JwtTokenOperation重新生成access token // 3. 返回新token和新的refresh token,实现token续签 }一个简化版的刷新接口应该是这样:校验refresh token在Redis里存在且未过期,然后重新调用JwtTokenOperation生成access token。要点是刷新后要让旧refresh token失效,避免token被反复使用。这块如果做深,还要考虑token轮换、设备下线通知、密码修改后吊销所有token,但这些都要结合你自己的存储和业务设计,源码包给出的是最干净的起点。
每次跑完这套流程,我都会把“登录-访问-刷新”三步用curl串一遍强制走完,哪怕只是一个人在本地调试也如此,因为token鉴权的问题往往在第二次、第三次请求时才会暴露。这也是拆这个源码包给我留下的习惯。希望帮到你。
本文还有配套的精品资源,点击获取