diff --git a/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs b/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs index b29406c..620d5a4 100644 --- a/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs +++ b/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using System; -using System.Reflection.Metadata; using System.Threading.Tasks; using Yh.Platform.Abstractions; using Yh.Platform.Auth; @@ -89,31 +88,37 @@ namespace YhCore.WebApi.Middleware "

重新登录

"); return; } - // API 渠道(/api/*、/mobiapi/*)的 AJAX/XHR 请求:未认证时返回 401 JSON,而非 302 重定向到 CAS。 + // API 渠道(/api/*、/mobiapi/*)的 AJAX/XHR 请求:未认证时不直接 302,而是复用框架 + // CasAuthenticationHandler 生成 CAS 登录 URL 后改写为 200 + JSON(code:302 + urlToRedirectTo)。 // 原因:浏览器 fetch/XHR 会跟随 302 跨域跳转到 sso.yunhaixingtong.com,而 CAS 登录页无 // Access-Control-Allow-Origin 头,导致 CORS 拦截(Response to preflight request ... blocked)。 - // 前端据此 401 做整页跳转登录(location.href = "/")。文档导航(浏览器地址栏 / Accept:text/html)仍走 302。 + // 前端据此 urlToRedirectTo 做整页跳转登录。文档导航(浏览器地址栏 / Accept:text/html)仍走 302。 if (IsProtectedPath(context.Request.Path.Value, sso) && IsAjaxRequest(context.Request)) { + // 复用框架 Challenge 统一构造 CAS 登录 URL:与 CasAuthenticationHandler.HandleChallengeAsync + // 使用同一 BuildServiceUrl 逻辑(AppBaseUrl + SignInCallbackPath + returnUrl 回跳), + // 自动获得 yh:sso:cas-server-url-prefix 未配置时返回 500、returnUrl 回跳、URL 编码等一致性保障, + // 避免本中间件重复手写 URL 拼接。RedirectUri 传入当前请求路径,登录成功后回跳原地址。 + await context.ChallengeAsync(YhAuthDefaults.CasScheme, new AuthenticationProperties + { + RedirectUri = BuildReturnUrl(context.Request) + }); + + // 拦截框架写入的 302 + Location,改写为 200 + JSON,避免 XHR 跟随跨域 302 被 CORS 拦截 + var location = context.Response.Headers["Location"].ToString(); context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.Headers.Remove("Location"); context.Response.ContentType = "application/json; charset=utf-8"; - - // 返回与文档导航 Challenge 一致的 CAS 登录地址(200 + JSON 而非 302,避免跨域跳转被 CORS 拦截)。 - // service 指向 signin-cas 回调,与 CasAuthenticationHandler.HandleChallengeAsync 的 BuildServiceUrl 一致 - // (AppBaseUrl + SignInCallbackPath),保证回调重建 service 时与登录时完全一致,ticket 校验才能通过。 - var casBase = (sso["CasServerUrlPrefix"] ?? string.Empty).TrimEnd('/'); - var root = string.IsNullOrEmpty(sso["AppBaseUrl"]) - ? (context.Request.Scheme + "://" + context.Request.Host.Value) - : sso["AppBaseUrl"].TrimEnd('/'); - var callbackPath = sso["SignInCallbackPath"] ?? "/signin-cas"; - var loginUrl = casBase + "/login?service=" + Uri.EscapeDataString(root + callbackPath); - - await context.Response.WriteAsync("{\"code\":302,\"urlToRedirectTo\":\"" + loginUrl + "\"}"); + await context.Response.WriteAsync("{\"code\":302,\"urlToRedirectTo\":\"" + location + "\"}"); return; } - // 未认证 → Challenge → 302 重定向 CAS 登录页(ticket 校验与回跳由新框架 CasAuthenticationHandler 处理) - await context.ChallengeAsync(YhAuthDefaults.CasScheme); + // 未认证 → Challenge → 302 重定向 CAS 登录页(ticket 校验与回跳由新框架 CasAuthenticationHandler 处理)。 + // RedirectUri 传入当前请求地址,实现登录成功后回跳原页面(对齐框架 returnUrl 语义)。 + await context.ChallengeAsync(YhAuthDefaults.CasScheme, new AuthenticationProperties + { + RedirectUri = BuildReturnUrl(context.Request) + }); return; } @@ -173,5 +178,17 @@ namespace YhCore.WebApi.Middleware return path.Equals(pattern, StringComparison.OrdinalIgnoreCase); } + + /// + /// 构造登录后回跳原地址(本地相对路径,含 query,不含 ticket)。 + /// 与框架 CasAuthenticationHandler 的 returnUrl 语义一致:必须是 / 开头的本地路径, + /// 否则框架 BuildServiceUrl 的 IsLocalUrl 校验会拒绝并回退到纯回调地址(防开放重定向)。 + /// + private static string BuildReturnUrl(HttpRequest request) + { + var path = request.Path.Value ?? "/"; + var query = request.QueryString.Value; + return string.IsNullOrEmpty(query) ? path : path + query; + } } }