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

相关推荐
ai小鬼头5 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
萧曵 丶6 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
老任与码6 小时前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba
华子w9089258597 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
星辰离彬7 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
GetcharZp9 小时前
彻底告别数据焦虑!这款开源神器 RustDesk,让你自建一个比向日葵、ToDesk 更安全的远程桌面
后端·rust
jack_yin10 小时前
Telegram DeepSeek Bot 管理平台 发布啦!
后端
小码编匠10 小时前
C# 上位机开发怎么学?给自动化工程师的建议
后端·c#·.net
库森学长10 小时前
面试官:发生OOM后,JVM还能运行吗?
jvm·后端·面试
转转技术团队10 小时前
二奢仓店的静默打印代理实现
java·后端