CAS service 全面直签原访问 URL:弃用 signin-cas 回调端点中继,回页面信息改走 ?redirectToReferer=<hex> 附加
build / build-push (push) Successful in 28s
build / build-push (push) Successful in 28s
This commit is contained in:
@@ -16,25 +16,29 @@ namespace YhCore.WebApi.Auth
|
||||
/// <summary>
|
||||
/// 自研 CAS 远程认证 handler(本地化自 Yh.Platform.Auth,改造为平台旧版登录 URL 格式):
|
||||
/// <list type="bullet">
|
||||
/// <item>Challenge:302 到 CAS 登录页。service 两种形态:
|
||||
/// (a) 无显式回跳语境(浏览器直访接口)——service = {AppBaseUrl}{当前 path}[?除 ticket 外 query],
|
||||
/// 即原访问 URL,与 Java notice 的 service 形态一致(service=http://host/module/api/xxx);
|
||||
/// (b) 显式回跳语境(前端 yschreferer/Referer 携带页面地址)——service =
|
||||
/// {AppBaseUrl}{SignInCallbackPath}[?returnUrl=<urlencoded 回跳地址>];回跳地址走 query
|
||||
/// 而非 Java 的路径矩阵参数 ;redirectToReferer=<hex>——ASP.NET Core 不支持矩阵参数,
|
||||
/// 分号会使 MVC 路由与 PathString.StartsWithSegments 双双失配,回调 404 覆盖掉本 handler 的 302 回跳;</item>
|
||||
/// <item>Challenge:302 到 CAS 登录页,service = {AppBaseUrl}{当前 path}[?除 ticket 外 query],即原访问 URL,
|
||||
/// 与 Java notice 的 service 形态一致(service=http://host/module/api/xxx);AppBaseUrl 含模块前缀
|
||||
/// (如 /docprint)时拼接结果天然带模块名。显式回跳语境(前端 yschreferer/Referer 携带页面地址)时
|
||||
/// 在该 service 上追加 query 参数 redirectToReferer=<hex(回跳地址)> 携带「登录后回页面」信息——
|
||||
/// 不再以 {SignInCallbackPath} 回调端点作 service(signin-cas 形态已弃用);hex 按 char 维度编码
|
||||
/// (对齐前端 strToHexCharCode / Java HexUtils),避免 query 内嵌完整 URL 需多层 urlencode。
|
||||
/// ASP.NET Core 无 Java 路径矩阵参数机制,回跳目标只能放 query。</item>
|
||||
/// <item>Authenticate:任意受保护路径带 ticket 的回调均按「path + 除 ticket 外的 query」重建 service
|
||||
/// 调 /serviceValidate 校验(登录时 service 为原 URL 时回调即原 URL + ticket;为回调端点时回调为
|
||||
/// {SignInCallbackPath}?returnUrl=…&ticket=…)。成功后写入 Cookie 登录态并 302 到
|
||||
/// returnUrl(显式回跳形态)/ 原 URL 去 ticket 版(原 URL 形态),兼容旧 ;redirectToReferer=<hex> 形态。</item>
|
||||
/// 调 /serviceValidate 校验(service 即登录时的原访问 URL,CAS 在末尾追加 ?ticket=/&ticket= 回跳)。
|
||||
/// 成功后写入 Cookie 登录态并 302 到 redirectToReferer(hex 解码,当前形态)/ returnUrl(旧中继形态
|
||||
/// 兼容)/ 原 URL 去 ticket 版(无回跳目标),兼容更早的 ;redirectToReferer=<hex> 路径段形态。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class CasAuthenticationHandler : AuthenticationHandler<CasAuthenticationOptions>
|
||||
{
|
||||
private const string RedirectToRefererMarker = ";redirectToReferer=";
|
||||
|
||||
/// <summary>旧中继形态(service={回调端点}?returnUrl=…)的回跳参数——已弃用,仅兼容历史 URL/缓存。</summary>
|
||||
private const string ReturnUrlParam = "returnUrl";
|
||||
|
||||
/// <summary>当前形态回跳参数:service=原访问 URL 时追加 ?redirectToReferer=<hex(回跳地址)>,登录后 CAS 原样回带。</summary>
|
||||
private const string RedirectToRefererQueryParam = "redirectToReferer";
|
||||
|
||||
private readonly CasServiceTicketValidator _validator;
|
||||
|
||||
public CasAuthenticationHandler(
|
||||
@@ -49,30 +53,12 @@ namespace YhCore.WebApi.Auth
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造 service 回调地址:{root}{SignInCallbackPath}[?returnUrl=<urlencoded(returnUrl)>]。
|
||||
/// 回跳地址允许两种:本地相对路径(/xxx),或与 service 根 host 同源的完整 http(s) URL
|
||||
/// (前端页完整地址,yschreferer 携带,含 #/ 路由片段——对齐 Java notice 语义),杜绝开放重定向。
|
||||
/// 注意:回跳地址走 query 而非路径矩阵参数——ASP.NET Core 不识别矩阵参数,分号会破坏
|
||||
/// 路由匹配与 StartsWithSegments 的段比较(Java Spring 因 UrlPathHelper 默认
|
||||
/// removeSemicolonContent=true 剥离分号内容才可用矩阵参数,.NET 无此机制)。
|
||||
/// </summary>
|
||||
private string BuildServiceUrl(string returnUrl)
|
||||
{
|
||||
var callback = BuildCallbackRoot() + Options.SignInCallbackPath;
|
||||
if (string.IsNullOrEmpty(returnUrl) || !IsAllowedReturnTarget(returnUrl, AllowedHost()))
|
||||
{
|
||||
return callback;
|
||||
}
|
||||
return callback + "?" + ReturnUrlParam + "=" + Uri.EscapeDataString(returnUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按当前请求重建登录时的 service:{root}{path}[?{除 ticket 外的 query}]。
|
||||
/// 按当前请求重建登录时的 service:{root}{path}[?{除 ticket 外的 query}],即原访问 URL(notice 形态)。
|
||||
/// CAS 回调会在原 service 后追加 ?ticket=(已有 query 时 &ticket=),校验时必须还原为登录时的
|
||||
/// 原始 service,故剥掉 ticket;其余参数按 EscapeDataString 重新编码,与 BuildServiceUrl 保持一致。
|
||||
/// 本方法同时充当「无显式回跳语境」的 Challenge service 构造(service = 原访问 URL,notice 形态);
|
||||
/// 此时 {root} 取自 AppBaseUrl(含 /docprint 等模块前缀),{path} 为 Kestrel 收到的接口路径,
|
||||
/// 拼接结果天然带模块名,不受反向代理剥离前缀影响。
|
||||
/// 原始 service,故剥掉 ticket;其余参数按 EscapeDataString 重新编码,与 Challenge 时构造保持一致。
|
||||
/// 本方法是 Challenge 与 Authenticate 两侧共用的 service 基准:Challenge 侧可能再追加
|
||||
/// redirectToReferer=<hex>(显式回跳语境);{root} 取自 AppBaseUrl(含 /docprint 等模块前缀),
|
||||
/// {path} 为 Kestrel 收到的接口路径,拼接结果天然带模块名,不受反向代理剥离前缀影响。
|
||||
/// </summary>
|
||||
private string BuildServiceUrlFromRequest()
|
||||
{
|
||||
@@ -84,7 +70,8 @@ namespace YhCore.WebApi.Auth
|
||||
return query.Count > 0 ? service + "?" + string.Join("&", query) : service;
|
||||
}
|
||||
|
||||
/// <summary>取回调 query 中的 returnUrl(本地相对路径或同源完整 http(s) URL,非法返回 null)。</summary>
|
||||
/// <summary>取回调 query 中的 returnUrl(旧中继形态兼容:service={回调端点}?returnUrl=… 的历史 URL/缓存;
|
||||
/// 本地相对路径或同源完整 http(s) URL,非法返回 null)。</summary>
|
||||
private string ResolveReturnUrl()
|
||||
{
|
||||
if (!Context.Request.Query.TryGetValue(ReturnUrlParam, out var value))
|
||||
@@ -95,6 +82,17 @@ namespace YhCore.WebApi.Auth
|
||||
return IsAllowedReturnTarget(url, AllowedHost()) ? url : null;
|
||||
}
|
||||
|
||||
/// <summary>读回调 query 的 redirectToReferer(当前形态:service=原访问 URL 时由 CAS 原样回带的
|
||||
/// hex 回跳地址)并解码;缺失或 hex 非法返回 null。合法性由调用方统一 IsAllowedReturnTarget 把关。</summary>
|
||||
private string ResolveQueryRedirectToReferer()
|
||||
{
|
||||
if (!Context.Request.Query.TryGetValue(RedirectToRefererQueryParam, out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return HexDecode(value.ToString());
|
||||
}
|
||||
|
||||
/// <summary>service 根地址:优先 AppBaseUrl 配置,为空时从当前请求推断。</summary>
|
||||
private string BuildCallbackRoot()
|
||||
{
|
||||
@@ -120,8 +118,8 @@ namespace YhCore.WebApi.Auth
|
||||
if (string.IsNullOrEmpty(target))
|
||||
{
|
||||
// 无显式回跳语境(浏览器/工具直访受保护接口,未带 yschreferer / Referer):
|
||||
// service 直接 = 原访问 URL(AppBaseUrl 前缀 + 当前 path + 除 ticket 外 query),
|
||||
// 与 Java notice 的 service 形态一致(service=http://host/module/api/xxx,无回调中继)。
|
||||
// service = 原访问 URL(AppBaseUrl 前缀 + 当前 path + 除 ticket 外 query),
|
||||
// 与 Java notice 的 service 形态一致(service=http://host/module/api/xxx,无附加参数)。
|
||||
// AppBaseUrl 带模块前缀(如 http://host/docprint)时,拼接结果天然含模块名,
|
||||
// 不受反向代理剥离前缀影响;登录成功后 CAS 回跳该 URL + ticket,由本 handler 校验并
|
||||
// 302 去掉 ticket 落回原地址。
|
||||
@@ -129,9 +127,14 @@ namespace YhCore.WebApi.Auth
|
||||
}
|
||||
else
|
||||
{
|
||||
// 显式回跳语境(前端 yschreferer 携带页面地址等):走回调端点 + returnUrl 中继形态
|
||||
// (ASP.NET Core 无 Java 矩阵参数机制,回跳地址只能放 query)。
|
||||
service = BuildServiceUrl(target);
|
||||
// 显式回跳语境(前端 yschreferer 携带页面地址等):service 仍 = 原访问 URL(notice 形态,
|
||||
// 不再以 {SignInCallbackPath} 回调端点作 service),仅追加 query 参数
|
||||
// redirectToReferer=<hex(回跳地址)> 携带「登录后回页面」信息——CAS 会把它作为 service 一部分
|
||||
// 在回调时原样带回,本 handler 校验成功后 hex 解码即得回跳目标。hex 按 char 维度编码,避免
|
||||
// query 内嵌完整 URL 需多层 urlencode(对齐前端 strToHexCharCode / Java HexUtils)。
|
||||
service = BuildServiceUrlFromRequest();
|
||||
service += (service.IndexOf('?') < 0 ? "?" : "&")
|
||||
+ RedirectToRefererQueryParam + "=" + HexEncode(target);
|
||||
}
|
||||
// Uri.EscapeDataString 会把 ; / : = 全部编码(%3B/%2F/%3A/%3D),与平台旧版系统生成的
|
||||
// CAS 登录 URL 逐字节一致(UrlEncoder.Encode 会保留 ; 字面量)
|
||||
@@ -189,12 +192,15 @@ namespace YhCore.WebApi.Auth
|
||||
Context.User = principal;
|
||||
|
||||
// 回跳原地址:
|
||||
// 1. query returnUrl(显式回跳中继形态的回调携带)→ 跳该目标;
|
||||
// 2. 旧 ;redirectToReferer=<hex> 回调(兼容);
|
||||
// 3. 都没有(service=原访问 URL 的 notice 形态:CAS 在 service 后追加 ?ticket= 回跳,
|
||||
// 1. query redirectToReferer=<hex>(当前形态:service=原访问 URL 时由 CAS 原样回带)→ 解码跳该目标;
|
||||
// 2. query returnUrl(旧中继形态 service={回调端点}?returnUrl=… 的历史 URL/缓存,兼容);
|
||||
// 3. 旧 ;redirectToReferer=<hex> 路径段(更早形态,兼容);
|
||||
// 4. 都没有(service=纯净原访问 URL 的 notice 形态:CAS 在 service 后追加 ?ticket= 回跳,
|
||||
// 本请求 Path + 除 ticket 外 query 即登录时原 URL)→ 302 去掉一次性 ticket 落回原地址。
|
||||
// 允许本地相对路径或同源完整 http(s) URL;非法(外站 host / 协议相对等)回根路径,防开放重定向。
|
||||
var referer = ResolveReturnUrl() ?? ParseRedirectToReferer(Context.Request.Path.Value);
|
||||
var referer = ResolveQueryRedirectToReferer()
|
||||
?? ResolveReturnUrl()
|
||||
?? ParseRedirectToReferer(Context.Request.Path.Value);
|
||||
string redirect;
|
||||
if (referer != null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user