目录
NotCheckJWTVersionAttribute.cs
JWT缺点
- 到期前,令牌无法被提前撤回。什么情况下需要撤回?用户被删除了、禁用了;令牌被盗用了;单设备登录。
- 需要JWT撤回的场景用传统Session更合适。
- 如果需要在JWT中实现,思路:用Redis保存状态,或者用refresh_token+access_token机制等。
方案
在用户表中增加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值自增;当服务器端收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT令牌过期了。
实现
-
为用户实体User类增加一个long类型的属性JWTVersion。
cspublic class MyUser : IdentityUser<long> { public string? WeChatAccout { get; set; } public long JWTVersions { get; set; } }
-
修改登录并发放令牌的代码,把用户的JWTVersion属性的值自增,并且把JWTVersion的值写入JWT令牌。
-
编写一个操作筛选器,统一实现对所有的控制器的操作方法中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的筛选器都要查询数据库,性能太低,可以用缓存进行优化。