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;
}