当前位置: 首页 > article >正文

ASP.NET Core JWT Version

目录

JWT缺点

方案

实现

Program.cs

IdentityHelper.cs

Controller

NotCheckJWTVersionAttribute.cs

JWTVersionCheckkFilter.cs

优化


JWT缺点

  1. 到期前,令牌无法被提前撤回。什么情况下需要撤回?用户被删除了、禁用了;令牌被盗用了;单设备登录。
  2. 需要JWT撤回的场景用传统Session更合适。
  3. 如果需要在JWT中实现,思路:用Redis保存状态,或者用refresh_token+access_token机制等。

方案

在用户表中增加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值自增;当服务器端收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT令牌过期了。

实现

  1. 为用户实体User类增加一个long类型的属性JWTVersion。
    public class MyUser : IdentityUser<long>
    {
        public string? WeChatAccout { get; set; }
        public long JWTVersions { get; set; }
    }
  2. 修改登录并发放令牌的代码,把用户的JWTVersion属性的值自增,并且把JWTVersion的值写入JWT令牌。
  3. 编写一个操作筛选器,统一实现对所有的控制器的操作方法中JWT令牌的检查操作。把JWTValidationFilter注册到Program.cs中MVC的全局筛选器中。

Program.cs

using Identity框架;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
//将Bearer身份验证添加到Scalar
builder.Services.AddOpenApi(opt =>
{
    opt.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
//添加数据库上下文
builder.Services.AddDbContext<MyDbContext>(opt =>
{
    string connStr = Environment.GetEnvironmentVariable("ConnStr");
    opt.UseSqlServer(connStr);
});
//添加Filter
builder.Services.Configure<MvcOptions>(opt =>
{
    opt.Filters.Add<JWTVersionCheckFilter>();//添加JWT版本检查ActionFilter
});
//添加Identity服务
builder.Services.AddDataProtection();
builder.Services.AddIdentityCore<MyUser>(options =>
{
    //设置密码规则,不需要数字,小写字母,大写字母,特殊字符,长度为6
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromSeconds(30);
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequiredLength = 6;
    options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
    options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
var idBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
idBuilder.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders()
    .AddRoleManager<RoleManager<MyRole>>().AddUserManager<UserManager<MyUser>>();
//添加JWT设置
builder.Services.Configure<JWTSettings>(builder.Configuration.GetSection("JWT"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt =>
{
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTSettings>();
    byte[] key = Encoding.UTF8.GetBytes(jwtOpt.SecKey);
    //设置对称秘钥
    var secKey = new SymmetricSecurityKey(key);
    //设置验证参数
    opt.TokenValidationParameters = new()
    {
        ValidateIssuer = false,//是否验证颁发者
        ValidateAudience = false,//是否验证订阅者
        ValidateLifetime = true,//是否验证生命周期
        ValidateIssuerSigningKey = true,//是否验证签名
        IssuerSigningKey = secKey//签名秘钥
    };
});

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

IdentityHelper.cs

public static class IdentityHelper
{
    public static async Task CheckAsync(this Task<IdentityResult> task)
    {
        var r = await task;
        if (!r.Succeeded)
        {
            throw new Exception(JsonSerializer.Serialize(r.Errors));
        }
    }
}

Controller

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Identity框架.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class DemoController : ControllerBase
    {
        private readonly UserManager<MyUser> userManager;
        private readonly RoleManager<MyRole> roleManager;
        private readonly IOptionsSnapshot<JWTSettings> jwtSettingsOpt;

        public DemoContrller(UserManager<MyUser> userManager, RoleManager<MyRole> roleManager, IOptionsSnapshot<JWTSettings> jwtSettingsOpt)
        {
            this.userManager = userManager;
            this.roleManager = roleManager;
            this.jwtSettingsOpt = jwtSettingsOpt;
        }

        [HttpPost]
        [NotCheckJWTVersion]
        public async Task<ActionResult<string>> Login(string userName, string password)
        {
            //根据用户名查找用户
            var user = await userManager.FindByNameAsync(userName);
            if (user == null)
            {
                return BadRequest("用户或密码错误1");
            }
            //判断是否登录成功,失败则记录失败次数
            if (await userManager.CheckPasswordAsync(user, password))
            {
                //登录成功,重置失败次数,CheckAsync判断操作是否成功,失败则抛出异常
                await userManager.ResetAccessFailedCountAsync(user).CheckAsync();
                //更新JWT版本号,防止旧JWT被使用
                user.JWTVersions++;
                await userManager.UpdateAsync(user);
                //身份验证声明
                List<Claim> claims = new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim("JWTVersions", user.JWTVersions.ToString())
                };
                //获取用户角色,添加到声明中
                var roles = await userManager.GetRolesAsync(user);
                foreach (var role in roles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, role));
                }
                //生成JWT
                string key = jwtSettingsOpt.Value.SecKey;
                DateTime expires = DateTime.Now.AddSeconds(jwtSettingsOpt.Value.ExpireSeconds);
                byte[] keyBytes = Encoding.UTF8.GetBytes(key);
                var secKey = new SymmetricSecurityKey(keyBytes);
                var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
                var tokenDescriptor = new JwtSecurityToken(
                    claims: claims,//声明
                    expires: expires,//过期时间
                    signingCredentials: credentials//签名凭据
                    );
                string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
                return jwt;
            }
            else
            {
                await userManager.AccessFailedAsync(user).CheckAsync();
                return BadRequest("用户或密码错误2");
            }
        }
    }
}

NotCheckJWTVersionAttribute.cs

[AttributeUsage(AttributeTargets.Method)]
public class NotCheckJWTVersionAttribute:Attribute
{
}

JWTVersionCheckkFilter.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Claims;

namespace Identity框架
{
    public class JWTVersionCheckFilter : IAsyncActionFilter
    {
        private readonly UserManager<MyUser> userManager;

        public JWTVersionCheckFilter(UserManager<MyUser> userManager)
        {
            this.userManager = userManager;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            //获取当前Action的特性,判断是否有NotCheckJWTVersionAttribute特性,如果有则不检查JWTVersion
            ControllerActionDescriptor controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
            if (controllerActionDescriptor == null)
            {
                await next();
                return;
            }
            if (controllerActionDescriptor.MethodInfo.GetCustomAttributes(typeof(NotCheckJWTVersionAttribute), true).Any()){
                await next();
                return;
            }
            //获取JWTVersion,不存在则返回400
            var claimJWTVersion = context.HttpContext.User.FindFirst("JWTVersions");
            if (claimJWTVersion == null)
            {
                context.Result = new ObjectResult("payload中JWTVersion不存在")
                {
                    StatusCode = 400
                };
                return;
            }
            //获取用户id,不存在则返回400
            var claimUserId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
            long userId = Convert.ToInt64(claimUserId.Value);
            //不用每次都查询数据库,可以从缓存中获取用户
            var user = await userManager.FindByIdAsync(userId.ToString());
            if (user == null)
            {
                context.Result = new ObjectResult("用户不存在")
                {
                    StatusCode = 400
                };
                return;
            }
            //判断JWTVersion是否过时
            long jwtVersionClient = Convert.ToInt64(claimJWTVersion.Value);
            if (user.JWTVersions > jwtVersionClient)
            {
                context.Result = new ObjectResult("客户端jwt过时")
                {
                    StatusCode = 400
                };
                return;
            }
            await next();
        }
    }
}

优化

每一次客户端和Controller的交互的时候,检查JWTVersion的筛选器都要查询数据库,性能太低,可以用缓存进行优化。


http://www.kler.cn/a/538402.html

相关文章:

  • 深度学习 交易预测 LSTM 层的神经元数量、训练轮数
  • 聚类算法概念、分类、特点及应用场景【机器学习】【无监督学习】
  • [ Spring ] Integrate Spring Boot Service Monitor Prometheus and Grafana
  • 【Android开发AI实战】选择目标跟踪基于opencv实现——运动跟踪
  • 开放式TCP/IP通信
  • Java | RESTful 接口规范
  • 【Emotion】打工路夜谈
  • 用 DeepSeek + Kimi 自动做 PPT,效率起飞
  • 鸿蒙 router.back()返回不到上个页面
  • [LeetCode]day17 349.两个数组的交集
  • 云计算架构师的学习成长路线
  • [C 语言篇】数据在内存中的存储
  • 2025牛客寒假算法基础集训营4(补题)
  • Swipe横滑与SwipeItem自定义横滑相互影响
  • 双向链表、内核链表和gdb(20250208)
  • Linux之kernel(1)系统基础理论(1)
  • FreeRTOS的事件组
  • 协议-RK-Gstreamer
  • 07苍穹外卖之redis缓存商品、购物车(redis案例缓存实现)
  • 【Windows】PowerShell 缓存区大小调节
  • LMM-3DP:集成 LMM 规划器和 3D 技能策略实现可泛化操作
  • 深入剖析 JVM 垃圾收集器之 CMS 和 G1
  • Golang:精通sync/atomic 包的Atomic 操作
  • 本地计算机上的MySQL80服务启动后停止某些服务在未由其他服务或程序使用时将自动停止(不需要清除数据)
  • 今日写题work
  • Https握手过程 (面试题)