From eb8f40f2ab932e1642baeb14b0dc009a743d0ad9 Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Tue, 9 Jun 2026 15:17:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20=E4=BC=98=E5=8C=96JWT?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=92=8CToken=E5=88=B7=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 调整JWT令牌过期时间配置,新增JwtTokenExpiryDays配置项 2. 分离Token存储前缀,区分管理后台和微信端Token 3. 新增Redis Token有效性校验逻辑,拦截无效/已注销的Token 4. 简化自动刷新逻辑,改为每次请求刷新Redis Token过期时间 5. 完善相关注释和代码结构 --- .../Auth/JwtHelper.cs | 2 +- .../DependencyInjectionExtensions.cs | 31 ++++++++ .../Middleware/JwtAutoRefreshMiddleware.cs | 79 ++++++++++++------- .../Settings/JwtSettings.cs | 7 +- 4 files changed, 89 insertions(+), 30 deletions(-) diff --git a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs index 71b8f0a..9bfa282 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs @@ -36,7 +36,7 @@ public static class JwtHelper issuer: settings.Issuer, audience: settings.Audience, claims: claims, - expires: DateTime.Now.AddMinutes(settings.ExpiryMinutes), + expires: DateTime.Now.AddDays(settings.JwtTokenExpiryDays), signingCredentials: credentials ); diff --git a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs index 19f6a2e..74e228f 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.Models.Settings; +using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; @@ -68,6 +69,36 @@ public static class DependencyInjectionExtensions ValidateLifetime = true, 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 已失效,请重新登录"); + } + }; }); } } diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs index f3f3681..c3a79f2 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs @@ -5,12 +5,18 @@ using System.IdentityModel.Tokens.Jwt; namespace QYZH.InteractiveMagazine.Infrastructure.Middleware; +/// +/// JWT 自动刷新中间件 +/// 功能: +/// 每次请求都刷新 Redis 中 Token 的过期时间(保持会话活跃) +/// 前端永远使用同一个 Token,无需处理 Token 刷新 +/// public class JwtAutoRefreshMiddleware { private readonly RequestDelegate _next; private readonly IConfiguration _configuration; - private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token"; - private const int RefreshThresholdMinutes = 10; + private const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token"; + private const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token"; public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration) { @@ -24,13 +30,13 @@ public class JwtAutoRefreshMiddleware if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")) { var token = authHeader.Substring("Bearer ".Length).Trim(); - await TryAutoRefreshTokenAsync(context, token); + await TryRefreshRedisTokenExpiryAsync(context, token); } await _next(context); } - private async Task TryAutoRefreshTokenAsync(HttpContext context, string token) + private async Task TryRefreshRedisTokenExpiryAsync(HttpContext context, string token) { try { @@ -43,36 +49,53 @@ public class JwtAutoRefreshMiddleware var expiryTime = jwtToken.ValidTo; 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; - 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(); - 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; + return; } + + 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(); + if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0) + { + return; + } + + // 每次请求都刷新 Redis 中 Token 的过期时间 + await RefreshRedisTokenExpiryAsync(userId, token, jwtSettings.ExpiryMinutes); } catch { // 忽略自动刷新异常,由后续认证中间件处理 } } + + /// + /// 刷新 Redis 中已存在 Token 的过期时间(保持会话活跃) + /// + 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)); + } + } } diff --git a/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs b/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs index ca8c2e9..0bc1d3a 100644 --- a/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs +++ b/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs @@ -21,7 +21,12 @@ public class JwtSettings public string? SecretKey { get; set; } /// - /// 过期时间(分钟) + /// 过期时间(分钟)- Redis 中 Token 的过期时间,每次请求会刷新 /// public int ExpiryMinutes { get; set; } + + /// + /// JWT 令牌本身的过期时间(分钟),建议设置较长(如 30 天),实际过期由 Redis 控制 + /// + public int JwtTokenExpiryDays { get; set; } = 30; }