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 无法提前撤回的问题。

相关推荐
红尘散仙1 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记2 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆3 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪3 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6163 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364573 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao4 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒5 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰6 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox6 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全