Asp.Net Core 通过JWT版本号实现JWT无法提前撤回的问题

文章目录


前言

ASP.NET Core 中解决 JWT 无法提前撤回的问题,需要结合服务器端状态管理机制来弥补 JWT 无状态的特性。

以下是基于版本号机制实现JWT提前失效的方案。

一、核心思想

通过在用户表中维护一个递增的版本号(JWTVersion),每次令牌颁发或撤销时更新版本号,验证时对比令牌中的版本号与数据库中的版本号。

二、实现步骤

1.用户表添加版本号字段

  1. 在用户表中新增 JWTVersion 字段(整数类型),初始值为 0。
csharp 复制代码
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;

namespace JWTWebAPI.Entity
{
    public class AspNetUsers:IdentityUser<long>
    {
        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; }

    }
}
  1. 数据库迁移,执行如下命令

    csharp 复制代码
    add-migration user_JWTVersion
    Update-Database

2.颁发令牌时包含版本号JWTVersion

  1. 代码如下(示例):

    csharp 复制代码
    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<AspNetUsers> userManager;
    
            public AuthService(IOptions<JwtSettings> jwtSettings, IUserRepository userRepository, UserManager<AspNetUsers> userManager)
            {
                _jwtSettings = jwtSettings.Value;
                _userRepository = userRepository;
                this.userManager = userManager;
            }
    
            public async Task<AuthResult> 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<AuthResult> RefreshToken(string token, string refreshToken)
            {
                throw new NotImplementedException();
            }
    
            private string GenerateJwtToken(IEnumerable<Claim> 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.验证令牌时校验版本号(过滤器)

  1. JWTVersionCheckFilter.cs

    csharp 复制代码
    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<AspNetUsers> userManager;
    
            public JWTVersionCheckFilter(UserManager<AspNetUsers> 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.注册过滤器

  1. 代码示例

    csharp 复制代码
    builder.Services.Configure<MvcOptions>(opt => {
        opt.Filters.Add<JWTVersionCheckFilter>();
    });

5.登录时不检查JWT版本号

  1. 创建NotCheckJWTAttribute.cs

    csharp 复制代码
    namespace JWTWebAPI.Extensions
    {
        [AttributeUsage(AttributeTargets.Method)]
        public class NotCheckJWTAttribute:Attribute
        {
        }
    }
  2. 在登录方法上标注[NotCheckJWT]

    csharp 复制代码
    [HttpPost]
    [NotCheckJWTAttribute]
    public async Task<IActionResult> Login([FromBody] LoginModel request)
    {
        var result = await _authService.Authenticate(request.Username, request.Password);
        if (result == null) return Unauthorized();
        return Ok(result);
    }

6.测试

  1. 调用Login方法获取第一个JWTToken:Token1
  2. 使用Token1调用方法XXX();
  3. 正常访问XXX();方法
  4. 再次调用Login方法获取第二个JWTToken:Token2
  5. 使用Token2调用方法XXX();
  6. 正常访问XXX();方法
  7. 使用Token1调用方法XXX();
  8. 提示""客户端JWT过时""

三、优点

  • 无需存储大量令牌数据,仅维护一个字段。
  • 撤销操作高效,仅需更新一次数据库。
  • 适用于高频撤销场景(如全局用户禁用)

总结

通过上述方案,可有效解决 JWT 无法提前撤回的问题。

相关推荐
你的人类朋友5 小时前
【操作系统】Unix和Linux是什么关系?
后端·操作系统·unix
uzong6 小时前
半小时打造七夕传统文化网站:Qoder AI编程实战记录
后端·ai编程
快乐就是哈哈哈6 小时前
从传统遍历到函数式编程:彻底掌握 Java Stream 流
后端
ningqw7 小时前
JWT 的使用
java·后端·springboot
追逐时光者7 小时前
精选 2 款 .NET 开源、实用的缓存框架,帮助开发者更轻松地处理系统缓存!
后端·.net
David爱编程8 小时前
指令重排与内存屏障:并发语义的隐形守护者
java·后端
胡gh9 小时前
数组开会:splice说它要动刀,map说它只想看看。
javascript·后端·面试
Pure_Eyes9 小时前
go 常见面试题
开发语言·后端·golang
Cisyam10 小时前
使用Bright Data API轻松构建LinkedIn职位数据采集系统
后端
float_六七10 小时前
Spring Boot 3为何强制要求Java 17?
java·spring boot·后端