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",