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

发布于:2025-05-30 ⋅ 阅读:(16) ⋅ 点赞:(0)


前言

在 ASP.NET Core 中解决 JWT 无法提前撤回的问题,需要结合服务器端状态管理机制来弥补 JWT 无状态的特性。
以下是基于版本号机制实现JWT提前失效的方案。

一、核心思想

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

二、实现步骤

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

  1. 在用户表中新增 JWTVersion 字段(整数类型),初始值为 0。
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. 数据库迁移,执行如下命令
    add-migration user_JWTVersion
    Update-Database
    

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

  1. 代码如下(示例):
    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
    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. 代码示例
    builder.Services.Configure<MvcOptions>(opt => {
        opt.Filters.Add<JWTVersionCheckFilter>();
    });
    

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

  1. 创建NotCheckJWTAttribute.cs
    namespace JWTWebAPI.Extensions
    {
        [AttributeUsage(AttributeTargets.Method)]
        public class NotCheckJWTAttribute:Attribute
        {
        }
    }
    
    
  2. 在登录方法上标注[NotCheckJWT]
    [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 无法提前撤回的问题。


网站公告

今日签到

点亮在社区的每一天
去签到