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

06-01 1351阅读

文章目录

  • 前言
  • 一、核心思想
  • 二、实现步骤
    • 1.用户表添加版本号字段
    • 2.颁发令牌时包含版本号JWTVersion
    • 3.验证令牌时校验版本号(过滤器)
    • 4.注册过滤器
    • 5.登录时不检查JWT版本号
    • 6.测试
    • 三、优点
    • 总结

      前言

      在 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
          {
              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 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.验证令牌时校验版本号(过滤器)

      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 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.注册过滤器

      1. 代码示例
        builder.Services.Configure(opt => {
            opt.Filters.Add();
        });
        

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

      1. 创建NotCheckJWTAttribute.cs
        namespace JWTWebAPI.Extensions
        {
            [AttributeUsage(AttributeTargets.Method)]
            public class NotCheckJWTAttribute:Attribute
            {
            }
        }
        
      2. 在登录方法上标注[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.测试

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

      三、优点

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

        总结

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

        Asp.Net Core 通过JWT版本号实现JWT无法提前撤回的问题
        (图片来源网络,侵删)
        Asp.Net Core 通过JWT版本号实现JWT无法提前撤回的问题
        (图片来源网络,侵删)
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码