矩阵分号段前置处理(对齐 Java removeSemicolonContent + notice 回跳语义):UseRouting 前剥离分号段杜绝 404;;redirectToReferer=<hex> 解析并 302 解码地址;带 ticket 登录回跳放行认证链路
build / build-push (push) Successful in 27s

This commit is contained in:
wanghu
2026-09-04 12:52:04 +08:00
parent ff31293df3
commit e9c4b64240
2 changed files with 143 additions and 0 deletions
@@ -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
{
/// <summary>
/// 路径矩阵分号段前置中间件(须注册在 UseRouting 之前)。
/// 对齐 Java notice 行为:Spring 路由前默认 UrlPathHelper.removeSemicolonContent=true 会剥离
/// 分号内容(矩阵参数),ASP.NET Core 不做——带 ';' 的路径进入 UseRouting 会匹配不到任何 action
/// 而 404。本中间件在路由前模拟该语义:
/// - 路径含 ;redirectToReferer=&lt;hex&gt;(service 分号矩阵段,CAS 登录引导时写入):按 notice
/// 语义解析 hex 并 302 到解码地址——覆盖"已登录刷新/直访带分号 URL"等 handler 未接管的场景,
/// 不再 404;hex 解码失败/目标非法则剥离分号段继续(杜绝开放重定向)。
/// - 其余含 ';' 的路径:剥离首个 ';' 起的分号段后继续,正常路由/保护判定不再 404。
/// 例外:请求带 ticket(CAS 登录回跳在途)→ 原样放行,不在此 302/剥离——须先由认证链路校验 ticket、
/// 写入登录 Cookie(成功后由 CasAuthenticationHandler 302 + SsoProtectionMiddleware 短路);
/// 剥离会破坏 serviceValidate 重建一致性,提前 302 会跳过 Cookie 建立导致死循环。
/// </summary>
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);
}
/// <summary>
/// 矩阵分号段回跳目标合法性(与 CasAuthenticationHandler.IsAllowedReturnTarget 同口径):
/// 拒绝含控制字符目标;本地相对路径(拒绝协议相对 // 与反斜杠变体);完整 http(s) URL 须与
/// AppBaseUrl host 一致或为本机 loopback(localhost/127.0.0.1/::1 联调);AppBaseUrl 未配置则不做
/// host 白名单。
/// </summary>
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;
}
/// <summary>十六进制 → 字符串(char 维度,每两位还原一个字符;奇数长/非法 hex 返回 null)。</summary>
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();
}
}
}
+5
View File
@@ -210,6 +210,11 @@ namespace YhCore.WebApi
// 放在 UseRouting 之前:静态文件请求直接短路,不进入 SSO/认证管道。
app.UseStaticFiles();
// 路径矩阵分号段前置处理(;redirectToReferer=<hex> 解析回跳 + 分号段剥离对齐 Spring
// removeSemicolonContent)。必须在 UseRouting 之前:路由按含分号路径匹配不到 action 会 404,
// 且此阶段改 Request.Path 才来得及影响路由选择。带 ticket 的登录回跳原样放行给认证链路。
app.UseMiddleware<PathMatrixSegmentMiddleware>();
// 管道顺序:先路由,再认证;CAS 回调必须在认证阶段消费 ticket 并写入 Cookie。
app.UseRouting();