From c342f543f7e08dd30f6fba60664855966c023195 Mon Sep 17 00:00:00 2001
From: glz <694770232@qq.com>
Date: Tue, 30 Jun 2026 09:15:52 +0800
Subject: [PATCH] =?UTF-8?q?fix=20=E7=99=BB=E5=BD=95=E6=8E=88=E6=9D=83?=
=?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Auth/JwtHelper.cs | 23 +++++++
.../DependencyInjectionExtensions.cs | 66 +++++++++++--------
.../Middleware/JwtAutoRefreshMiddleware.cs | 59 ++++++++---------
.../WeChatAuthService.cs | 13 ++--
.../appsettings.json | 3 +-
5 files changed, 94 insertions(+), 70 deletions(-)
diff --git a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs
index 0cd55d0..1321366 100644
--- a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs
+++ b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs
@@ -11,6 +11,9 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Auth;
///
public static class JwtHelper
{
+ public const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
+ public const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
+
///
/// 生成JWT令牌(管理端 / 单用户场景)
///
@@ -60,6 +63,26 @@ public static class JwtHelper
///
public const string WxUserIdClaimType = "WxUserId";
+ public static string BuildAdminTokenKey(string userId)
+ {
+ return $"{AdminTokenKeyPrefix}:{userId}";
+ }
+
+ public static string BuildAdminTokenKey(long userId)
+ {
+ return BuildAdminTokenKey(userId.ToString());
+ }
+
+ public static string BuildWeChatTokenKey(string wxUserId, string userId)
+ {
+ return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}";
+ }
+
+ public static string BuildWeChatTokenKey(long wxUserId, long userId)
+ {
+ return BuildWeChatTokenKey(wxUserId.ToString(), userId.ToString());
+ }
+
private static string BuildToken(Claim[] claims, JwtSettings settings)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!));
diff --git a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs
index 74e228f..ebfc4e7 100644
--- a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs
+++ b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
+using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Models.Settings;
using System.Security.Claims;
@@ -16,34 +17,23 @@ using System.Text.Encodings.Web;
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
///
-/// 统一服务注册扩展
+/// Infrastructure service registration extensions.
///
public static class DependencyInjectionExtensions
{
///
- /// 注册基础设施服务
+ /// Registers infrastructure services.
///
- /// 服务集合
- /// 配置
- /// 运行环境
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
{
AddJwtAuthentication(services, configuration, environment);
services.AddTransient();
services.AddTransient();
-
}
- ///
- /// 配置JWT认证
- ///
- /// 服务集合
- /// 配置
- /// 运行环境
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
{
- // 开发环境下跳过 JWT 验证
if (environment?.IsDevelopment() == true)
{
services.AddAuthentication("NoAuth")
@@ -74,37 +64,56 @@ public static class DependencyInjectionExtensions
{
OnTokenValidated = async context =>
{
+ var currentToken = GetBearerToken(context);
+ if (string.IsNullOrEmpty(currentToken))
+ {
+ context.Fail("Invalid token");
+ return;
+ }
+
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
- context.Fail("无效的 Token");
+ context.Fail("Invalid token");
return;
}
- // 检查管理后台 Token
- var adminToken = await RedisHelper.GetAsync($"InteractiveMagazine:AdminAuth:Token:{userId}");
- if (!string.IsNullOrEmpty(adminToken))
+ var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
+ var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
+ if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
{
return;
}
- // 检查微信端 Token
- var wechatToken = await RedisHelper.GetAsync($"InteractiveMagazine:WeChatAuth:Token:{userId}");
- if (!string.IsNullOrEmpty(wechatToken))
+ if (!string.IsNullOrEmpty(wxUserId))
{
- return;
+ var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
+ if (wechatToken == currentToken)
+ {
+ return;
+ }
}
- // Redis 中不存在任何 Token,认证失败
- context.Fail("Token 已失效,请重新登录");
+ context.Fail("Token expired, please login again");
}
};
});
}
+
+ private static string? GetBearerToken(TokenValidatedContext context)
+ {
+ var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return authHeader.Substring("Bearer ".Length).Trim();
+ }
}
///
-/// 开发环境免认证处理器
+/// Authentication handler used only in development.
///
public class NoAuthHandler : AuthenticationHandler
{
@@ -115,14 +124,13 @@ public class NoAuthHandler : AuthenticationHandler
protected override Task HandleAuthenticateAsync()
{
- // 开发环境下始终认证成功
var claims = new[]
{
- new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "DevUser"),
- new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "0")
+ new Claim(ClaimTypes.Name, "DevUser"),
+ new Claim(ClaimTypes.NameIdentifier, "0")
};
- var identity = new System.Security.Claims.ClaimsIdentity(claims, Scheme.Name);
- var principal = new System.Security.Claims.ClaimsPrincipal(identity);
+ var identity = new ClaimsIdentity(claims, Scheme.Name);
+ var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs
index eb1ef24..fc5aa69 100644
--- a/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs
+++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs
@@ -3,21 +3,17 @@ using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
///
-/// JWT 自动刷新中间件
-/// 功能:
-/// 每次请求都刷新 Redis 中 Token 的过期时间(保持会话活跃)
-/// 前端永远使用同一个 Token,无需处理 Token 刷新
+/// Refreshes the Redis session TTL for the current JWT when it is still the active token.
///
public class JwtAutoRefreshMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
- private const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
- private const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
{
@@ -31,13 +27,13 @@ public class JwtAutoRefreshMiddleware
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
- await TryRefreshRedisTokenExpiryAsync(context, token);
+ await TryRefreshRedisTokenExpiryAsync(token);
}
await _next(context);
}
- private async Task TryRefreshRedisTokenExpiryAsync(HttpContext context, string token)
+ private async Task TryRefreshRedisTokenExpiryAsync(string token)
{
try
{
@@ -47,60 +43,57 @@ public class JwtAutoRefreshMiddleware
return;
}
- var expiryTime = jwtToken.ValidTo;
- var remainingTime = expiryTime - DateTime.Now;
-
- // JWT 已过期,不处理
- if (remainingTime <= TimeSpan.Zero)
+ if (jwtToken.ValidTo <= DateTime.UtcNow)
{
return;
}
- var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
+ var userId = GetClaimValue(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId);
if (string.IsNullOrEmpty(userId))
{
return;
}
- var wxUserId = jwtToken.Claims.FirstOrDefault(c => c.Type == JwtHelper.WxUserIdClaimType)?.Value;
- if (string.IsNullOrEmpty(wxUserId))
- {
- return;
- }
+
var jwtSettings = _configuration.GetSection("JwtSettings").Get();
if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0)
{
return;
}
- // 每次请求都刷新 Redis 中 Token 的过期时间
- await RefreshRedisTokenExpiryAsync(wxUserId,userId, token, jwtSettings.ExpiryMinutes);
+ var wxUserId = GetClaimValue(jwtToken, JwtHelper.WxUserIdClaimType);
+ await RefreshRedisTokenExpiryAsync(wxUserId, userId, token, jwtSettings.ExpiryMinutes);
}
catch
{
- // 忽略自动刷新异常,由后续认证中间件处理
+ // Ignore refresh failures. Authentication middleware will validate the request later.
}
}
- ///
- /// 刷新 Redis 中已存在 Token 的过期时间(保持会话活跃)
- ///
- private async Task RefreshRedisTokenExpiryAsync(string wxUserId,string userId, string currentToken, int expiryMinutes)
+ private static async Task RefreshRedisTokenExpiryAsync(string? wxUserId, string userId, string currentToken, int expiryMinutes)
{
- // 检查 Admin Token
- var adminTokenKey = $"{AdminTokenKeyPrefix}:{userId}";
+ var adminTokenKey = JwtHelper.BuildAdminTokenKey(userId);
var adminToken = await RedisHelper.GetAsync(adminTokenKey);
- if (!string.IsNullOrEmpty(adminToken))
+ if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
{
- await RedisHelper.SetAsync(adminTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
+ await RedisHelper.SetAsync(adminTokenKey, adminToken, TimeSpan.FromMinutes(expiryMinutes));
return;
}
- // 检查 WeChat Token
- var wechatTokenKey = $"{WeChatTokenKeyPrefix}:{wxUserId}{userId}";
+ if (string.IsNullOrEmpty(wxUserId))
+ {
+ return;
+ }
+
+ var wechatTokenKey = JwtHelper.BuildWeChatTokenKey(wxUserId, userId);
var wechatToken = await RedisHelper.GetAsync(wechatTokenKey);
- if (!string.IsNullOrEmpty(wechatToken))
+ if (wechatToken == currentToken)
{
await RedisHelper.SetAsync(wechatTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
}
}
+
+ private static string? GetClaimValue(JwtSecurityToken jwtToken, params string[] claimTypes)
+ {
+ return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type))?.Value;
+ }
}
diff --git a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs
index b70aa77..a6d5498 100644
--- a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs
+++ b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs
@@ -24,7 +24,6 @@ public class WeChatAuthService(
IPetService petService)
: BaseRepository, IWeChatAuthService
{
- private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
private const string AccessTokenCacheKey = "InteractiveMagazine:WeChat:AccessToken";
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
private const string GetAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
@@ -85,10 +84,10 @@ public class WeChatAuthService(
UpdatedAt = DateTime.Now
};
- var wxUserId = await wxUserRepository.Insertable(wxUser).ExecuteReturnIdentityAsync();
- wxUser.Id = wxUserId;
+ await wxUserRepository.InsertAsync(wxUser);
- logger.LogInformation("WxUser 创建成功,WxUserId: {WxUserId}, OpenId: {OpenId}", wxUserId, wxResponse.OpenId);
+
+ logger.LogInformation("WxUser 创建成功,WxUserId: {WxUserId}, OpenId: {OpenId}", wxUser.Id, wxResponse.OpenId);
}
else
{
@@ -201,8 +200,8 @@ public class WeChatAuthService(
var token = JwtHelper.GenerateToken(wxUserId, refreshedUser.Id, refreshedUser.Name ?? string.Empty, jwtSettings);
// 清除旧 Redis Token,写入新 Token
- await RedisHelper.DelAsync($"{TokenKeyPrefix}:{currentUserId}");
- await RedisHelper.SetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
+ await RedisHelper.DelAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, currentUserId));
+ await RedisHelper.SetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, refreshedUser.Id), token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
return new WeChatSwitchUserOutput
{
@@ -327,7 +326,7 @@ public class WeChatAuthService(
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken(wxUser.Id, userId, userName, jwtSettings);
- await RedisHelper.SetAsync($"{TokenKeyPrefix}:{wxUser.Id}{userId}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
+ await RedisHelper.SetAsync(JwtHelper.BuildWeChatTokenKey(wxUser.Id, userId), token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
return new WeChatLoginOutput
{
diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.json b/QYZH.InteractiveMagazine.WebApi/appsettings.json
index 781e74d..87c9381 100644
--- a/QYZH.InteractiveMagazine.WebApi/appsettings.json
+++ b/QYZH.InteractiveMagazine.WebApi/appsettings.json
@@ -6,7 +6,8 @@
"Issuer": "QYZH.InteractiveMagazine",
"Audience": "QYZH.InteractiveMagazine",
"SecretKey": "zG7pLqR9xVw2bN8fYtHk3mPc5sA1dF6eUjW4gXhC7vB",
- "ExpiryMinutes": 120
+ "ExpiryMinutes": 120,
+ "JwtTokenExpiryDays": 30
},
"RedisSettings": {
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5",