@@ -45,7 +45,8 @@ namespace YhCore.WebApi.Auth
|
||||
|
||||
/// <summary>
|
||||
/// 构造 service 回调地址:{root}{SignInCallbackPath}[?returnUrl=<urlencoded(returnUrl)>]。
|
||||
/// 回跳地址只允许本地相对路径(/xxx),杜绝开放重定向。
|
||||
/// 回跳地址允许两种:本地相对路径(/xxx),或与 service 根 host 同源的完整 http(s) URL
|
||||
/// (前端页完整地址,yschreferer 携带,含 #/ 路由片段——对齐 Java notice 语义),杜绝开放重定向。
|
||||
/// 注意:回跳地址走 query 而非路径矩阵参数——ASP.NET Core 不识别矩阵参数,分号会破坏
|
||||
/// 路由匹配与 StartsWithSegments 的段比较(Java Spring 因 UrlPathHelper 默认
|
||||
/// removeSemicolonContent=true 剥离分号内容才可用矩阵参数,.NET 无此机制)。
|
||||
@@ -53,7 +54,7 @@ namespace YhCore.WebApi.Auth
|
||||
private string BuildServiceUrl(string returnUrl)
|
||||
{
|
||||
var callback = BuildCallbackRoot() + Options.SignInCallbackPath;
|
||||
if (string.IsNullOrEmpty(returnUrl) || !IsLocalUrl(returnUrl))
|
||||
if (string.IsNullOrEmpty(returnUrl) || !IsAllowedReturnTarget(returnUrl, AllowedHost()))
|
||||
{
|
||||
return callback;
|
||||
}
|
||||
@@ -75,7 +76,7 @@ namespace YhCore.WebApi.Auth
|
||||
return query.Count > 0 ? service + "?" + string.Join("&", query) : service;
|
||||
}
|
||||
|
||||
/// <summary>取回调 query 中的 returnUrl(仅本地相对路径,非法返回 null)。</summary>
|
||||
/// <summary>取回调 query 中的 returnUrl(本地相对路径或同源完整 http(s) URL,非法返回 null)。</summary>
|
||||
private string ResolveReturnUrl()
|
||||
{
|
||||
if (!Context.Request.Query.TryGetValue(ReturnUrlParam, out var value))
|
||||
@@ -83,7 +84,7 @@ namespace YhCore.WebApi.Auth
|
||||
return null;
|
||||
}
|
||||
var url = value.ToString();
|
||||
return IsLocalUrl(url) ? url : null;
|
||||
return IsAllowedReturnTarget(url, AllowedHost()) ? url : null;
|
||||
}
|
||||
|
||||
/// <summary>service 根地址:优先 AppBaseUrl 配置,为空时从当前请求推断。</summary>
|
||||
@@ -163,9 +164,9 @@ namespace YhCore.WebApi.Auth
|
||||
Context.User = principal;
|
||||
|
||||
// 回跳原地址:优先 query 的 returnUrl(当前形态),兼容旧 ;redirectToReferer=<hex> 回调。
|
||||
// 仅限本地相对路径,防开放重定向;非法回根路径
|
||||
// 允许本地相对路径或同源完整 http(s) URL;非法(外站 host / 协议相对等)回根路径,防开放重定向。
|
||||
var referer = ResolveReturnUrl() ?? ParseRedirectToReferer(Context.Request.Path.Value);
|
||||
var redirect = referer != null && IsLocalUrl(referer) ? referer : "/";
|
||||
var redirect = IsAllowedReturnTarget(referer, AllowedHost()) ? referer : "/";
|
||||
Response.Redirect(redirect);
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
@@ -185,47 +186,78 @@ namespace YhCore.WebApi.Auth
|
||||
return HexDecode(path.Substring(marker + RedirectToRefererMarker.Length));
|
||||
}
|
||||
|
||||
/// <summary>UTF-8 字节 → 小写十六进制(对齐平台旧版登录 URL 的 redirectToReferer 编码)。</summary>
|
||||
/// <summary>字符串 → 小写十六进制(char 维度,与前端 strToHexCharCode / Java HexUtils.stringToHex 一致:
|
||||
/// 逐字符取码点转两位 hex;ASCII 范围内结果与 UTF-8 字节编码相同)。</summary>
|
||||
private static string HexEncode(string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
var sb = new StringBuilder(bytes.Length * 2);
|
||||
foreach (var b in bytes)
|
||||
var sb = new StringBuilder();
|
||||
foreach (var ch in value ?? string.Empty)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
sb.Append(((int)ch).ToString("x2"));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>小写/大写十六进制 → UTF-8 字符串;长度为奇数或含非十六进制字符时返回 null。</summary>
|
||||
/// <summary>小写/大写十六进制 → 字符串(char 维度解码,对齐前端 yschreferer / Java hexToString);
|
||||
/// 长度为奇数或含非十六进制字符时返回 null。</summary>
|
||||
private static string HexDecode(string hex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex) || hex.Length % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var bytes = new byte[hex.Length / 2];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
var sb = new StringBuilder(hex.Length / 2);
|
||||
for (var i = 0; i < hex.Length; i += 2)
|
||||
{
|
||||
if (!byte.TryParse(hex.Substring(i * 2, 2), NumberStyles.HexNumber,
|
||||
CultureInfo.InvariantCulture, out var b))
|
||||
if (!int.TryParse(hex.Substring(i, 2), NumberStyles.HexNumber,
|
||||
CultureInfo.InvariantCulture, out var code))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[i] = b;
|
||||
sb.Append((char)code);
|
||||
}
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>仅放行本地相对路径(/xxx);拒绝外站绝对地址(http(s)://…)、协议相对地址(//host)、反斜杠变体。</summary>
|
||||
private static bool IsLocalUrl(string url)
|
||||
/// <summary>
|
||||
/// 判定回跳目标是否允许:
|
||||
/// 1. 本地相对路径(/xxx 开头):拒绝协议相对地址(//host)与反斜杠变体(/\…)——原有安全口径;
|
||||
/// 2. 完整 http(s) URL:host 必须与 service 根 host(AppBaseUrl 或当前请求)一致——供前端页完整
|
||||
/// 地址(yschreferer 携带,含 #/ 路由片段)作回跳目标,杜绝开放重定向到任意外站。
|
||||
/// 其余情况(null/空、非 http(s) 协议、外站 host)一律拒绝。
|
||||
/// </summary>
|
||||
private static bool IsAllowedReturnTarget(string url, string allowedHost)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url) || url[0] != '/')
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return !url.StartsWith("//", StringComparison.Ordinal)
|
||||
&& !url.StartsWith("/\\", StringComparison.Ordinal);
|
||||
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;
|
||||
}
|
||||
return string.IsNullOrEmpty(allowedHost)
|
||||
|| string.Equals(uri.Host, allowedHost, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>service 根地址的 host(回跳白名单基准):AppBaseUrl 未配置时取当前请求 host。</summary>
|
||||
private string AllowedHost()
|
||||
{
|
||||
var root = Options.AppBaseUrl;
|
||||
if (string.IsNullOrEmpty(root))
|
||||
{
|
||||
root = Context.Request.Scheme + "://" + Context.Request.Host.Value;
|
||||
}
|
||||
return Uri.TryCreate(root, UriKind.Absolute, out var uri) ? uri.Host : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user