Asp.Net Core 通过JWT版本号实现JWT无法提前撤回的问题
文章目录
- 前言
- 一、核心思想
- 二、实现步骤
- 1.用户表添加版本号字段
- 2.颁发令牌时包含版本号JWTVersion
- 3.验证令牌时校验版本号(过滤器)
- 4.注册过滤器
- 5.登录时不检查JWT版本号
- 6.测试
- 三、优点
- 总结
前言
在 ASP.NET Core 中解决 JWT 无法提前撤回的问题,需要结合服务器端状态管理机制来弥补 JWT 无状态的特性。
以下是基于版本号机制实现JWT提前失效的方案。
一、核心思想
通过在用户表中维护一个递增的版本号(JWTVersion),每次令牌颁发或撤销时更新版本号,验证时对比令牌中的版本号与数据库中的版本号。
二、实现步骤
1.用户表添加版本号字段
- 在用户表中新增 JWTVersion 字段(整数类型),初始值为 0。
using Microsoft.AspNetCore.Identity; using System.ComponentModel.DataAnnotations; namespace JWTWebAPI.Entity { public class AspNetUsers:IdentityUser { public DateTime CreateTime { get; set; } [Required] [MaxLength(20)] public string Role { get; set; } public string? RefreshToken { get; set; } public DateTime? RefreshTokenExpiry { get; set; } // 权限存储(示例使用逗号分隔字符串) public string Permissions { get; set; } = "content.read,profile.update"; //JWT版本号 public long JWTVersion { get; set; } } }
- 数据库迁移,执行如下命令
add-migration user_JWTVersion Update-Database
2.颁发令牌时包含版本号JWTVersion
- 代码如下(示例):
using JWTWebAPI.Entity; using JWTWebAPI.Interface; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; namespace JWTWebAPI.Repository { public class AuthService : IAuthService { private readonly JwtSettings _jwtSettings; private readonly IUserRepository _userRepository; private readonly UserManager userManager; public AuthService(IOptions jwtSettings, IUserRepository userRepository, UserManager userManager) { _jwtSettings = jwtSettings.Value; _userRepository = userRepository; this.userManager = userManager; } public async Task Authenticate(string username, string password) { var user = await _userRepository.GetUserByCredentials(username, password); if (user == null) return null; user.JWTVersion++; await userManager.UpdateAsync(user); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Role, user.Role), // 用户角色 new Claim("permissions",string.Join(",", user.Permissions)), new Claim("JWTVersion",user.JWTVersion.ToString()) }; var token = GenerateJwtToken(claims); var refreshToken = GenerateRefreshToken(); await _userRepository.SaveRefreshToken(user.Id, refreshToken, DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)); return new AuthResult { Token = token, RefreshToken = refreshToken, ExpiresIn = _jwtSettings.ExpirationMinutes * 60 }; } public Task RefreshToken(string token, string refreshToken) { throw new NotImplementedException(); } private string GenerateJwtToken(IEnumerable claims) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } private static string GenerateRefreshToken() { var randomNumber = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } } }
3.验证令牌时校验版本号(过滤器)
- JWTVersionCheckFilter.cs
using JWTWebAPI.Entity; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using System.Security.Claims; namespace JWTWebAPI.Extensions { public class JWTVersionCheckFilter : IAsyncActionFilter { private readonly UserManager userManager; public JWTVersionCheckFilter(UserManager userManager) { this.userManager = userManager; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { ControllerActionDescriptor? atrActionDes= context.ActionDescriptor as ControllerActionDescriptor; if (atrActionDes == null) { await next(); return; } if (atrActionDes.MethodInfo.GetCustomAttributes(typeof(NotCheckJWTAttribute), true).Any()) { await next(); return; } var claimJWTVersion = context.HttpContext.User.FindFirst("JWTVersion"); if (claimJWTVersion == null) { context.Result = new ObjectResult("payload中没有JWTVersion") { StatusCode=400}; return; } var clientJwtVersion=Convert.ToInt64(claimJWTVersion.Value); string userId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value; var user=await userManager.FindByIdAsync(userId); if (user == null) { context.Result = new ObjectResult("无用户信息") { StatusCode = 400 }; return; } if (user.JWTVersion > clientJwtVersion) { context.Result = new ObjectResult("客户端JWT过时") { StatusCode = 400 }; return; } await next(); } } }
4.注册过滤器
- 代码示例
builder.Services.Configure(opt => { opt.Filters.Add(); });
5.登录时不检查JWT版本号
- 创建NotCheckJWTAttribute.cs
namespace JWTWebAPI.Extensions { [AttributeUsage(AttributeTargets.Method)] public class NotCheckJWTAttribute:Attribute { } }
- 在登录方法上标注[NotCheckJWT]
[HttpPost] [NotCheckJWTAttribute] public async Task Login([FromBody] LoginModel request) { var result = await _authService.Authenticate(request.Username, request.Password); if (result == null) return Unauthorized(); return Ok(result); }
6.测试
- 调用Login方法获取第一个JWTToken:Token1
- 使用Token1调用方法XXX();
- 正常访问XXX();方法
- 再次调用Login方法获取第二个JWTToken:Token2
- 使用Token2调用方法XXX();
- 正常访问XXX();方法
- 使用Token1调用方法XXX();
- 提示“”客户端JWT过时“”
三、优点
- 无需存储大量令牌数据,仅维护一个字段。
- 撤销操作高效,仅需更新一次数据库。
- 适用于高频撤销场景(如全局用户禁用)
总结
通过上述方案,可有效解决 JWT 无法提前撤回的问题。
(图片来源网络,侵删)(图片来源网络,侵删)
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。