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

相关推荐
lixww.cn3 小时前
ASP.NET Core SignalR的协议协商
asp.net core·signalr
lixww.cn14 小时前
ASP.NET Core SignalR的分布式部署
redis·消息队列·asp.net core·signalr
lixww.cn1 天前
ASP.NET Core对JWT的封装
asp.net core·jwt·authorize
lixww.cn2 天前
ASP.NET Core JWT
asp.net core·jwt
lixww.cn3 天前
ASP.NET Core标识框架Identity
asp.net core·rbac·identity
lixww.cn4 天前
ASP.NET Core中间件Markdown转换器
中间件·markdown·asp.net core
kong790692812 天前
电商系统-用户认证(三)基于公钥解析JWT令牌
jwt·用户认证·解析jwt令牌
一只淡水鱼6614 天前
【spring】集成JWT实现登录验证
java·spring·jwt
梦幻加菲猫24 天前
JWT在线解密/解码 - 加菲工具
jwt·jwt 在线解密/解码·jwt在线解密·jwt在线解码