refactor(auth): 优化JWT认证和Token刷新逻辑
1. 调整JWT令牌过期时间配置,新增JwtTokenExpiryDays配置项 2. 分离Token存储前缀,区分管理后台和微信端Token 3. 新增Redis Token有效性校验逻辑,拦截无效/已注销的Token 4. 简化自动刷新逻辑,改为每次请求刷新Redis Token过期时间 5. 完善相关注释和代码结构
This commit is contained in:
@ -36,7 +36,7 @@ public static class JwtHelper
|
|||||||
issuer: settings.Issuer,
|
issuer: settings.Issuer,
|
||||||
audience: settings.Audience,
|
audience: settings.Audience,
|
||||||
claims: claims,
|
claims: claims,
|
||||||
expires: DateTime.Now.AddMinutes(settings.ExpiryMinutes),
|
expires: DateTime.Now.AddDays(settings.JwtTokenExpiryDays),
|
||||||
signingCredentials: credentials
|
signingCredentials: credentials
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||||
using QYZH.InteractiveMagazine.Models.Settings;
|
using QYZH.InteractiveMagazine.Models.Settings;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
@ -68,6 +69,36 @@ public static class DependencyInjectionExtensions
|
|||||||
ValidateLifetime = true,
|
ValidateLifetime = true,
|
||||||
ClockSkew = TimeSpan.Zero
|
ClockSkew = TimeSpan.Zero
|
||||||
};
|
};
|
||||||
|
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnTokenValidated = async context =>
|
||||||
|
{
|
||||||
|
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
context.Fail("无效的 Token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管理后台 Token
|
||||||
|
var adminToken = await RedisHelper.GetAsync($"InteractiveMagazine:AdminAuth:Token:{userId}");
|
||||||
|
if (!string.IsNullOrEmpty(adminToken))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查微信端 Token
|
||||||
|
var wechatToken = await RedisHelper.GetAsync($"InteractiveMagazine:WeChatAuth:Token:{userId}");
|
||||||
|
if (!string.IsNullOrEmpty(wechatToken))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 中不存在任何 Token,认证失败
|
||||||
|
context.Fail("Token 已失效,请重新登录");
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,18 @@ using System.IdentityModel.Tokens.Jwt;
|
|||||||
|
|
||||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 自动刷新中间件
|
||||||
|
/// 功能:
|
||||||
|
/// 每次请求都刷新 Redis 中 Token 的过期时间(保持会话活跃)
|
||||||
|
/// 前端永远使用同一个 Token,无需处理 Token 刷新
|
||||||
|
/// </summary>
|
||||||
public class JwtAutoRefreshMiddleware
|
public class JwtAutoRefreshMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
|
private const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
|
||||||
private const int RefreshThresholdMinutes = 10;
|
private const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||||
|
|
||||||
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
|
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
@ -24,13 +30,13 @@ public class JwtAutoRefreshMiddleware
|
|||||||
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
|
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
|
||||||
{
|
{
|
||||||
var token = authHeader.Substring("Bearer ".Length).Trim();
|
var token = authHeader.Substring("Bearer ".Length).Trim();
|
||||||
await TryAutoRefreshTokenAsync(context, token);
|
await TryRefreshRedisTokenExpiryAsync(context, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _next(context);
|
await _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryAutoRefreshTokenAsync(HttpContext context, string token)
|
private async Task TryRefreshRedisTokenExpiryAsync(HttpContext context, string token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -43,36 +49,53 @@ public class JwtAutoRefreshMiddleware
|
|||||||
var expiryTime = jwtToken.ValidTo;
|
var expiryTime = jwtToken.ValidTo;
|
||||||
var remainingTime = expiryTime - DateTime.Now;
|
var remainingTime = expiryTime - DateTime.Now;
|
||||||
|
|
||||||
if (remainingTime <= TimeSpan.FromMinutes(RefreshThresholdMinutes) && remainingTime > TimeSpan.Zero)
|
// JWT 已过期,不处理
|
||||||
|
if (remainingTime <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
return;
|
||||||
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(userName))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
|
|
||||||
if (jwtSettings == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken(
|
|
||||||
long.Parse(userId), userName, jwtSettings);
|
|
||||||
|
|
||||||
await RedisHelper.SetAsync(
|
|
||||||
$"{TokenKeyPrefix}:{userId}",
|
|
||||||
newToken,
|
|
||||||
TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
|
||||||
|
|
||||||
context.Response.Headers["X-New-Token"] = newToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
|
||||||
|
if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每次请求都刷新 Redis 中 Token 的过期时间
|
||||||
|
await RefreshRedisTokenExpiryAsync(userId, token, jwtSettings.ExpiryMinutes);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// 忽略自动刷新异常,由后续认证中间件处理
|
// 忽略自动刷新异常,由后续认证中间件处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新 Redis 中已存在 Token 的过期时间(保持会话活跃)
|
||||||
|
/// </summary>
|
||||||
|
private async Task RefreshRedisTokenExpiryAsync(string userId, string currentToken, int expiryMinutes)
|
||||||
|
{
|
||||||
|
// 检查 Admin Token
|
||||||
|
var adminTokenKey = $"{AdminTokenKeyPrefix}:{userId}";
|
||||||
|
var adminToken = await RedisHelper.GetAsync(adminTokenKey);
|
||||||
|
if (!string.IsNullOrEmpty(adminToken))
|
||||||
|
{
|
||||||
|
await RedisHelper.SetAsync(adminTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 WeChat Token
|
||||||
|
var wechatTokenKey = $"{WeChatTokenKeyPrefix}:{userId}";
|
||||||
|
var wechatToken = await RedisHelper.GetAsync(wechatTokenKey);
|
||||||
|
if (!string.IsNullOrEmpty(wechatToken))
|
||||||
|
{
|
||||||
|
await RedisHelper.SetAsync(wechatTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,12 @@ public class JwtSettings
|
|||||||
public string? SecretKey { get; set; }
|
public string? SecretKey { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 过期时间(分钟)
|
/// 过期时间(分钟)- Redis 中 Token 的过期时间,每次请求会刷新
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ExpiryMinutes { get; set; }
|
public int ExpiryMinutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 令牌本身的过期时间(分钟),建议设置较长(如 30 天),实际过期由 Redis 控制
|
||||||
|
/// </summary>
|
||||||
|
public int JwtTokenExpiryDays { get; set; } = 30;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user