- 修改了 RefreshTokenMiddleware 中的响应逻辑,将直接写入错误消息改为重定向到登录页面 - 移除了状态码设置、内容类型设置和错误消息写入 - 添加了重定向到 /Account/Login 的功能以提高用户体验 feat(menu): 添加资金通知书菜单项并调整顺序 - 在结算管理菜单中新增了资金通知书功能入口 - 将资金通知书菜单项移动到资金审批之后的位置 - 确保菜单结构与权限配置一致
181 lines
8.9 KiB
C#
181 lines
8.9 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Token续期中间件
|
|
/// 用于在用户有操作时自动续期登录状态
|
|
/// </summary>
|
|
public class RefreshTokenMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
// 续期阈值设为Token过期时间的20%,避免过于频繁的续期
|
|
private static int RefreshThreshold => (int)(AuthHelper.ExpireInSeconds * 0.2);
|
|
|
|
/// <summary>
|
|
/// 构造函数
|
|
/// </summary>
|
|
/// <param name="next">下一个中间件</param>
|
|
public RefreshTokenMiddleware(RequestDelegate next)
|
|
{
|
|
_next = next;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理请求
|
|
/// </summary>
|
|
/// <param name="context">HTTP上下文</param>
|
|
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);
|
|
var tokenPreview = hasToken && token.Length > 20 ? token.Substring(0, 20) + "..." : token;
|
|
logger.Info($"[中间件] 请求: {path}, HasToken: {hasToken}, TokenPreview: {tokenPreview}");
|
|
|
|
// 跳过登录相关页面和静态资源,避免黑名单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.Info($"[中间件] 白名单跳过: {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.Info($"[中间件] 黑名单检查 - JTI: {jti}, Blacklisted: {blacklisted != null}");
|
|
if (blacklisted != null)
|
|
{
|
|
logger.Info($"[中间件] Token在黑名单中,拒绝访问 - JTI: {jti}, Path: {path}");
|
|
context.Response.Cookies.Delete("Access-Token");
|
|
context.Response.Cookies.Delete("Access-Token-Encrypt");
|
|
context.Response.Redirect("/Account/Login");
|
|
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.CreateJwtToken2(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
|
|
context.Response.Cookies.Delete("Access-Token");
|
|
context.Response.Cookies.Delete("Access-Token-Encrypt");
|
|
Console.WriteLine("Token expired, cookies cleared");
|
|
}
|
|
catch (SecurityTokenInvalidSignatureException)
|
|
{
|
|
// Token签名无效,可能是被篡改
|
|
context.Response.Cookies.Delete("Access-Token");
|
|
context.Response.Cookies.Delete("Access-Token-Encrypt");
|
|
Console.WriteLine("Invalid token signature, cookies cleared");
|
|
}
|
|
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);
|
|
context.Response.Cookies.Delete("Access-Token");
|
|
context.Response.Cookies.Delete("Access-Token-Encrypt");
|
|
Console.WriteLine("Token audience validation failed, cookies cleared");
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
} |