diff --git a/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs b/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs index 1dc4ff8..5e2ac44 100644 --- a/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs +++ b/src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs @@ -18,6 +18,8 @@ namespace YhCore.WebApi.Middleware /// 直接访问回调(无 ticket)进入登录流程;携带失效/已消费 ticket 的回调则返回 401 错误页, /// 不再次 Challenge——避免“回调→登录→新票据→再失败”无限重定向循环(自动登录型 CAS 下必现); /// 回调校验成功(已认证 + 302)则短路管道,防止 MVC 端点匹配 404 覆盖 handler 的回跳 302; + /// - 受保护 API 的 AJAX/XHR 请求:复用电框架 Challenge 生成 CAS 登录 URL 后改写为 200 + JSON + /// (code:302 + urlToRedirectTo),避免 XHR 跟随跨域 302 被 CORS 拦截;文档导航仍走 302; /// - Swagger 路径始终放行,方便本地接口调试; /// - /oapi 开放渠道始终放行:其准入由 AppAuthMiddleware 签名验签负责(X-App-Key / X-Timestamp / /// X-Signature 票据),无需用户登录态,SSO 登录保护永不覆盖该渠道(即使 yh:sso:urlPattern 误配 @@ -38,19 +40,15 @@ namespace YhCore.WebApi.Middleware public async Task InvokeAsync(HttpContext context) { var sso = _configuration.GetSection("yh:sso"); - var isSwaggerPath = context.Request.Path.StartsWithSegments("/swagger"); - var isOapiPath = context.Request.Path.StartsWithSegments("/oapi"); - var isRootEntry = context.Request.Path == "/"; - var isCasCallback = context.Request.Path.StartsWithSegments(sso["signInCallbackPath"] ?? "/signin-cas"); + var path = context.Request.Path; - // 已认证(含 Cookie 恢复登录态)直接放行 + // 1. 已认证(含 Cookie 恢复登录态)直接放行。 + // 回调路径刚完成票据校验:CasAuthenticationHandler 已写入 Cookie 并 Response.Redirect(302 + Location), + // 但 handler 返回 NoResult 不短路管道——若继续 _next 走 MVC,/signin-cas 无对应 action 会 404 覆盖 302, + // 浏览器看到"登录成功却 404"。此处已认证 + 已 302 即短路,保住回跳响应。 if (context.User?.Identity?.IsAuthenticated == true) { - // 回调路径刚完成票据校验:CasAuthenticationHandler 已写入 Cookie 并 Response.Redirect(302 + Location), - // 但 handler 返回 NoResult 不短路管道——若继续 _next 走 MVC 端点匹配,/signin-cas 无对应 action, - // 会返回 404 覆盖 302,浏览器看到"登录成功却 404"。此处已认证 + 已 302 即短路,保住回跳响应。 - if (isCasCallback && context.Response.StatusCode == 302 && - !string.IsNullOrEmpty(context.Response.Headers["Location"])) + if (IsCasCallback(path, sso) && IsRedirect(context.Response)) { return; } @@ -58,72 +56,111 @@ namespace YhCore.WebApi.Middleware return; } - // oapi 开放渠道:无需用户登录,准入由 AppAuthMiddleware 签名验签负责(票据 = 签名头), - // 未带票据的请求由 AppAuthMiddleware 返回 401(而非 SSO 302),保证直接访问语义。 - if (isOapiPath) + // 2. oapi 开放渠道:无需用户登录,准入由 AppAuthMiddleware 签名验签负责(票据 = 签名头), + // 未带票据的请求由 AppAuthMiddleware 返回 401(而非 SSO 302),保证直接访问语义。 + if (path.StartsWithSegments("/oapi")) { await _next(context); return; } - var enabled = sso["enabled"] != null - ? bool.TryParse(sso["enabled"], out var en) && en - : _configuration.GetValue("Yh:Sso:Enabled", false); - - if (enabled && !isSwaggerPath && - (isRootEntry || isCasCallback || IsProtectedPath(context.Request.Path.Value, sso))) + // 3. Swagger 路径始终放行,方便本地接口调试。 + if (path.StartsWithSegments("/swagger")) { - // 请求携带 ticket 却仍未认证 → 说明 CAS 票据校验失败(一次性票据已被消费/过期, - // 或 service 与服务端登记不一致)。此时若再次 Challenge,自动登录型 CAS 会立刻签发 - // 新票据跳回 → 再次失败 → 无限重定向循环。直接返回 401 错误页(带重新登录入口)打破循环。 - if (context.Request.Query.ContainsKey("ticket")) - { - context.Response.StatusCode = 401; - context.Response.ContentType = "text/html; charset=utf-8"; - await context.Response.WriteAsync( - "
CAS 登录票据为一次性凭证,刷新或重复使用同一地址会导致校验失败。
" + - ""); - return; - } - // 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)。 - // 前端据此 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"; - await context.Response.WriteAsync("{\"code\":302,\"urlToRedirectTo\":\"" + location + "\"}"); - } - - // 未认证 → Challenge → 302 重定向 CAS 登录页(ticket 校验与回跳由新框架 CasAuthenticationHandler 处理)。 - // RedirectUri 传入当前请求地址,实现登录成功后回跳原页面(对齐框架 returnUrl 语义)。 - await context.ChallengeAsync(YhAuthDefaults.CasScheme, new AuthenticationProperties - { - RedirectUri = BuildReturnUrl(context.Request) - }); + await _next(context); return; } - await _next(context); + // 4. SSO 未启用 → 所有路径匿名放行。 + if (!_configuration.GetValue("yh:sso:enabled", false)) + { + await _next(context); + return; + } + + // 5. 仅保护根路径(浏览器 SSO 入口)、CAS 回调路径、以及匹配 urlPattern/mobiApiPattern 的 API 路径。 + var isRootEntry = path == "/"; + var isCasCallback = IsCasCallback(path, sso); + if (!isRootEntry && !isCasCallback && !IsProtectedPath(path.Value, sso)) + { + await _next(context); + return; + } + + // 6. 请求携带 ticket 却仍未认证 → 说明 CAS 票据校验失败(一次性票据已被消费/过期, + // 或 service 与服务端登记不一致)。此时若再次 Challenge,自动登录型 CAS 会立刻签发 + // 新票据跳回 → 再次失败 → 无限重定向循环。直接返回 401 错误页(带重新登录入口)打破循环。 + if (context.Request.Query.ContainsKey("ticket")) + { + await WriteTicketErrorAsync(context); + return; + } + + // 7. 未认证 → Challenge → 交框架 CasAuthenticationHandler 处理(302 到 CAS 或改写 JSON)。 + await ChallengeAsync(context, sso, isCasCallback); } + ///CAS 登录票据为一次性凭证,刷新或重复使用同一地址会导致校验失败。
" + + ""); + } + + ///