feat(auth): 实现JWT令牌黑名单机制

- 在退出登录时清除JWT Token Cookie并将其加入黑名单
- 添加JTI声明到JWT令牌用于唯一标识和黑名单管理
- 在中间件中验证令牌时检查黑名单状态并拒绝已注销的令牌
- 实现令牌立即失效功能防止已退出用户继续访问系统
This commit is contained in:
hjhan
2026-03-23 17:52:15 +08:00
parent 276d53f986
commit f182b880c1
3 changed files with 53 additions and 1 deletions
+8 -1
View File
@@ -56,9 +56,16 @@ namespace YLErp.Helpers
{
var key = new SymmetricSecurityKey(Convert.FromBase64String(JwtSecretKey));
// 添加 JTI (JWT ID) 用于黑名单机制
var claims = new ClaimsIdentity(identity);
if (!claims.HasClaim(c => c.Type == "jti"))
{
claims.AddClaim(new Claim("jti", Guid.NewGuid().ToString("N")));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(identity),
Subject = claims,
Expires = DateTime.UtcNow.AddSeconds(ExpireInSeconds),
IssuedAt = DateTime.Now,
Issuer = JwtIssuer,
+29
View File
@@ -311,6 +311,35 @@ namespace YLErp.Web.Controllers
HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
logger.Info($"[退出登录] SignOutAsync 已调用");
// 清除 JWT Token Cookie
Response.Cookies.Delete("Access-Token");
Response.Cookies.Delete("Access-Token-Encrypt");
logger.Info($"[退出登录] JWT Token Cookie 已清除");
// 将当前 JWT Token 加入黑名单(使其立即失效)
if (Request.Cookies.TryGetValue("Access-Token", out var accessToken) && !string.IsNullOrEmpty(accessToken))
{
try
{
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadJwtToken(accessToken);
var jti = jwtToken.Claims.FirstOrDefault(c => c.Type == "jti")?.Value ?? accessToken.Substring(0, 32);
var expiry = jwtToken.ValidTo;
var ttl = expiry - DateTime.UtcNow;
if (ttl.TotalSeconds > 0)
{
// 将 Token 加入黑名单,过期时间与 Token 过期时间相同
Server.CacheProvider.Set($"token_blacklist:{jti}", userId.ToString(), ttl);
logger.Info($"[退出登录] Token 已加入黑名单 - JTI: {jti}, 过期时间: {expiry:yyyy-MM-dd HH:mm:ss}");
}
}
catch (Exception ex)
{
logger.Error($"[退出登录] Token 加入黑名单失败", ex);
}
}
if (SsoLoginConfig.SSO_Enable)
{
switch (loginName?.ToLowerInvariant())
@@ -54,6 +54,22 @@ namespace YLErp.Web.Middleware
var tokenHandler = new JwtSecurityTokenHandler();
// 先解析token获取JTI,检查是否在黑名单中
var jwtTokenForCheck = tokenHandler.ReadJwtToken(token);
var jti = jwtTokenForCheck.Claims.FirstOrDefault(c => c.Type == "jti")?.Value ?? token.Substring(0, 32);
// 检查token是否在黑名单中
var blacklisted = Server.CacheProvider.Get($"token_blacklist:{jti}");
if (blacklisted != null)
{
LogFactory.GetLogger("RefreshTokenMiddleware").Info($"[Token验证] Token已在黑名单中,拒绝访问 - JTI: {jti}");
context.Response.Cookies.Delete("Access-Token");
context.Response.Cookies.Delete("Access-Token-Encrypt");
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Token已失效,请重新登录");
return;
}
// 验证并解析token
var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken);
var jwtToken = validatedToken as JwtSecurityToken;