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。

    cs 复制代码
    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

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

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

cs 复制代码
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 DemoContrller : 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

cs 复制代码
[AttributeUsage(AttributeTargets.Method)]
public class NotCheckJWTVersionAttribute:Attribute
{
}

JWTVersionCheckkFilter.cs

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的筛选器都要查询数据库,性能太低,可以用缓存进行优化。

相关推荐
图图图图爱睡觉16 天前
大白话解释认证JWT是什么 有什么用 怎么用
jwt
天上掉下来个程小白20 天前
登录-10.Filter-登录校验过滤器
spring boot·后端·spring·filter·登录校验
等待的L先生1 个月前
HttpServlet详解
http·javaee·interceptor·filter·event·httpservlet
汤米尼克1 个月前
板块一 Servlet编程:第九节 过滤器全解 来自【汤米尼克的JAVAEE全套教程专栏】
servlet·java-ee·filter
lixww.cn1 个月前
ASP.NET Core用MediatR实现领域事件
ddd·asp.net core·mediatr
lixww.cn1 个月前
ASP.NET Core SignalR向部分客户端发消息
javascript·websocket·vue·asp.net core·signalr
lixww.cn1 个月前
ASP.NET Core SignalR的协议协商
asp.net core·signalr
lixww.cn1 个月前
ASP.NET Core SignalR的分布式部署
redis·消息队列·asp.net core·signalr
lixww.cn1 个月前
ASP.NET Core对JWT的封装
asp.net core·jwt·authorize