矩阵分号段前置处理(对齐 Java removeSemicolonContent + notice 回跳语义):UseRouting 前剥离分号段杜绝 404;;redirectToReferer=<hex> 解析并 302 解码地址;带 ticket 登录回跳放行认证链路
build / build-push (push) Successful in 27s
build / build-push (push) Successful in 27s
This commit is contained in:
@@ -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=<hex>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user