using Microsoft.AspNetCore.Http; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Threading.Tasks; using YLErp.Helpers; using YLErp; using System; namespace YLErp.Web.Middleware { /// /// Token续期中间件 /// 用于在用户有操作时自动续期登录状态 /// public class RefreshTokenMiddleware { private readonly RequestDelegate _next; // 续期阈值设为Token过期时间的20%,避免过于频繁的续期 private static int RefreshThreshold => (int)(AuthHelper.ExpireInSeconds * 0.2); /// /// 构造函数 /// /// 下一个中间件 public RefreshTokenMiddleware(RequestDelegate next) { _next = next; } /// /// 统一拒绝请求:清除Cookie并返回401或跳转登录页 /// private static void RejectRequest(HttpContext context) { context.Response.Cookies.Delete("Access-Token"); context.Response.Cookies.Delete("Access-Token-Encrypt"); var isAjax = context.Request.Headers["X-Requested-With"] == "XMLHttpRequest" || !context.Request.Headers["Accept"].ToString().Contains("text/html"); if (isAjax) { context.Response.StatusCode = 401; } else { context.Response.Redirect("/Account/Login"); } } /// /// 处理请求 /// /// HTTP上下文 public async Task InvokeAsync(HttpContext context) { // 获取当前请求路径 var path = context.Request.Path.Value?.ToLowerInvariant() ?? ""; var logger = LogFactory.GetLogger("RefreshTokenMiddleware"); // 记录请求信息(仅调试时使用) var hasToken = context.Request.Cookies.TryGetValue("Access-Token", out var token); // logger.Debug($"[中间件] 请求: {path}, HasToken: {hasToken}"); // 高频请求时注释掉 // 跳过登录相关页面和静态资源,避免黑名单Token影响登录流程 // 注意:/account/login 包含 /account/loginhandle (POST登录请求) if (path.StartsWith("/account/login") || path.StartsWith("/account/logout") || path.StartsWith("/account/captcha") || path.StartsWith("/account/callback") || // SSO回调地址 path.StartsWith("/content/") || path.StartsWith("/scripts/") || path.StartsWith("/css/") || path.StartsWith("/images/") || path.StartsWith("/favicon.ico")) { // logger.Debug($"[中间件] 白名单跳过: {path}"); // 高频请求时注释掉 await _next(context); return; } // 检查是否有Access-Token(使用已声明的token变量) if (hasToken) { try { // 配置token验证参数 var validationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(AuthHelper.JwtSecretKey)), ValidIssuer = AuthHelper.JwtIssuer, ValidAudience = AuthHelper.JwtAudience, ClockSkew = TimeSpan.Zero // 不容忍时钟偏差 }; var tokenHandler = new JwtSecurityTokenHandler(); // 先解析token获取JTI,检查是否在黑名单中 var jwtTokenForCheck = tokenHandler.ReadJwtToken(token); var jti = jwtTokenForCheck.Claims.FirstOrDefault(c => c.Type == "jti")?.Value ?? token.Substring(0, 32); // 检查token是否在黑名单中 var blacklisted = Server.CacheProvider.Get($"token_blacklist:{jti}"); // logger.Debug($"[中间件] 黑名单检查 - JTI: {jti}, Blacklisted: {blacklisted != null}"); // 高频请求时注释掉 if (blacklisted != null) { logger.Info($"[中间件] Token在黑名单中,拒绝访问 - JTI: {jti}, Path: {path}"); RejectRequest(context); return; } // 验证并解析token var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); var jwtToken = validatedToken as JwtSecurityToken; // 检查token是否接近过期 var expiration = jwtToken.ValidTo; var timeUntilExpiry = expiration - DateTime.UtcNow; // 如果剩余时间少于阈值,自动续期 if (timeUntilExpiry.TotalSeconds < RefreshThreshold) { // 从token中获取用户信息 var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == "userId")?.Value; var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == "account")?.Value; var userToken = jwtToken.Claims.FirstOrDefault(c => c.Type == "uuid")?.Value; if (!string.IsNullOrEmpty(userId) && !string.IsNullOrEmpty(userName)) { // 创建新的identity var identity = UserManager.CreateIdentity(userId, userName, userToken); // 生成新的token var newToken = AuthHelper.CreateJwtToken(identity); // 更新cookie context.Response.Cookies.Append("Access-Token", newToken, new CookieOptions { MaxAge = TimeSpan.FromSeconds(AuthHelper.ExpireInSeconds), HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict }); context.Response.Cookies.Append("Access-Token-Encrypt", YLErp.Helpers.DataProtectHelper.Encrypt(newToken), new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict }); // 记录续期日志 Console.WriteLine($"Token refreshed for user: {userName}, new expiry: {DateTime.UtcNow.AddSeconds(AuthHelper.ExpireInSeconds)}"); } } } catch (SecurityTokenExpiredException) { // Token已过期,清除cookie RejectRequest(context); return; } catch (SecurityTokenInvalidSignatureException) { // Token签名无效,可能是被篡改 RejectRequest(context); return; } catch (SecurityTokenValidationException ex) when (ex.Message.Contains("IDX10206")) { // Token audience 验证失败,可能是旧token没有audience LogFactory.GetLogger("RefreshTokenMiddleware").Error($"[Token刷新] Audience验证失败 - Token: {token?.Substring(0, Math.Min(20, token?.Length ?? 0))}..., ValidIssuer: {AuthHelper.JwtIssuer}, ValidAudience: {AuthHelper.JwtAudience}", ex); RejectRequest(context); return; } catch (Exception ex) { // 记录其他异常 logger.Error($"[Token刷新] 未知错误 - Token: {token?.Substring(0, Math.Min(20, token?.Length ?? 0))}..., 异常类型: {ex.GetType().Name}", ex); Console.WriteLine($"Token refresh error: {ex.Message}"); // 继续处理请求,不影响正常流程 } } // 处理后续请求 await _next(context); } } }