diff --git a/src/YhCore.WebApi/Middleware/PathMatrixSegmentMiddleware.cs b/src/YhCore.WebApi/Middleware/PathMatrixSegmentMiddleware.cs
new file mode 100644
index 0000000..9e72710
--- /dev/null
+++ b/src/YhCore.WebApi/Middleware/PathMatrixSegmentMiddleware.cs
@@ -0,0 +1,138 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Globalization;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace YhCore.WebApi.Middleware
+{
+ ///
+ /// 路径矩阵分号段前置中间件(须注册在 UseRouting 之前)。
+ /// 对齐 Java notice 行为:Spring 路由前默认 UrlPathHelper.removeSemicolonContent=true 会剥离
+ /// 分号内容(矩阵参数),ASP.NET Core 不做——带 ';' 的路径进入 UseRouting 会匹配不到任何 action
+ /// 而 404。本中间件在路由前模拟该语义:
+ /// - 路径含 ;redirectToReferer=<hex>(service 分号矩阵段,CAS 登录引导时写入):按 notice
+ /// 语义解析 hex 并 302 到解码地址——覆盖"已登录刷新/直访带分号 URL"等 handler 未接管的场景,
+ /// 不再 404;hex 解码失败/目标非法则剥离分号段继续(杜绝开放重定向)。
+ /// - 其余含 ';' 的路径:剥离首个 ';' 起的分号段后继续,正常路由/保护判定不再 404。
+ /// 例外:请求带 ticket(CAS 登录回跳在途)→ 原样放行,不在此 302/剥离——须先由认证链路校验 ticket、
+ /// 写入登录 Cookie(成功后由 CasAuthenticationHandler 302 + SsoProtectionMiddleware 短路);
+ /// 剥离会破坏 serviceValidate 重建一致性,提前 302 会跳过 Cookie 建立导致死循环。
+ ///
+ public class PathMatrixSegmentMiddleware
+ {
+ private const string RedirectToRefererMarker = ";redirectToReferer=";
+
+ private readonly RequestDelegate _next;
+ private readonly IConfiguration _configuration;
+
+ public PathMatrixSegmentMiddleware(RequestDelegate next, IConfiguration configuration)
+ {
+ _next = next;
+ _configuration = configuration;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var rawPath = context.Request.Path.Value ?? string.Empty;
+ var semi = rawPath.IndexOf(';');
+ if (semi < 0)
+ {
+ await _next(context);
+ return;
+ }
+
+ // CAS 登录回跳在途(service 含分号段 + ticket):原样放行给认证链路。
+ if (context.Request.Query.ContainsKey("ticket"))
+ {
+ await _next(context);
+ return;
+ }
+
+ var marker = rawPath.IndexOf(RedirectToRefererMarker, StringComparison.OrdinalIgnoreCase);
+ if (marker >= 0)
+ {
+ var hex = rawPath.Substring(marker + RedirectToRefererMarker.Length);
+ var term = hex.IndexOfAny(new[] { ';', '?', '#' });
+ if (term >= 0)
+ {
+ hex = hex.Substring(0, term);
+ }
+ var target = HexToString(hex);
+ if (!string.IsNullOrEmpty(target) && IsAllowedMatrixTarget(target, _configuration["yh:sso:AppBaseUrl"]))
+ {
+ context.Response.Redirect(target);
+ return;
+ }
+ // 解码失败/非法目标:不跳转(防开放重定向),落入下方剥离分号段走正常流程。
+ }
+
+ context.Request.Path = new PathString(rawPath.Substring(0, semi));
+ await _next(context);
+ }
+
+ ///
+ /// 矩阵分号段回跳目标合法性(与 CasAuthenticationHandler.IsAllowedReturnTarget 同口径):
+ /// 拒绝含控制字符目标;本地相对路径(拒绝协议相对 // 与反斜杠变体);完整 http(s) URL 须与
+ /// AppBaseUrl host 一致或为本机 loopback(localhost/127.0.0.1/::1 联调);AppBaseUrl 未配置则不做
+ /// host 白名单。
+ ///
+ private static bool IsAllowedMatrixTarget(string url, string appBaseUrl)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ return false;
+ }
+ foreach (var ch in url)
+ {
+ if (ch < 0x20 || ch == 0x7f)
+ {
+ return false;
+ }
+ }
+ if (url[0] == '/')
+ {
+ return !url.StartsWith("//", StringComparison.Ordinal)
+ && !url.StartsWith("/\\", StringComparison.Ordinal);
+ }
+ if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
+ {
+ return false;
+ }
+ if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
+ {
+ return false;
+ }
+ string allowedHost = null;
+ if (!string.IsNullOrWhiteSpace(appBaseUrl)
+ && Uri.TryCreate(appBaseUrl, UriKind.Absolute, out var baseUri))
+ {
+ allowedHost = baseUri.Host;
+ }
+ return string.IsNullOrEmpty(allowedHost)
+ || string.Equals(uri.Host, allowedHost, StringComparison.OrdinalIgnoreCase)
+ || uri.IsLoopback;
+ }
+
+ /// 十六进制 → 字符串(char 维度,每两位还原一个字符;奇数长/非法 hex 返回 null)。
+ private static string HexToString(string hex)
+ {
+ if (string.IsNullOrEmpty(hex) || hex.Length % 2 != 0)
+ {
+ return null;
+ }
+ var sb = new StringBuilder(hex.Length / 2);
+ for (var i = 0; i < hex.Length; i += 2)
+ {
+ if (!int.TryParse(hex.Substring(i, 2), NumberStyles.HexNumber,
+ CultureInfo.InvariantCulture, out var code))
+ {
+ return null;
+ }
+ sb.Append((char)code);
+ }
+ return sb.ToString();
+ }
+ }
+}
diff --git a/src/YhCore.WebApi/Startup.cs b/src/YhCore.WebApi/Startup.cs
index 6697e65..4ceb4db 100644
--- a/src/YhCore.WebApi/Startup.cs
+++ b/src/YhCore.WebApi/Startup.cs
@@ -210,6 +210,11 @@ namespace YhCore.WebApi
// 放在 UseRouting 之前:静态文件请求直接短路,不进入 SSO/认证管道。
app.UseStaticFiles();
+ // 路径矩阵分号段前置处理(;redirectToReferer= 解析回跳 + 分号段剥离对齐 Spring
+ // removeSemicolonContent)。必须在 UseRouting 之前:路由按含分号路径匹配不到 action 会 404,
+ // 且此阶段改 Request.Path 才来得及影响路由选择。带 ticket 的登录回跳原样放行给认证链路。
+ app.UseMiddleware();
+
// 管道顺序:先路由,再认证;CAS 回调必须在认证阶段消费 ticket 并写入 Cookie。
app.UseRouting();