bag
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Yh.Platform.Auth;
|
||||
|
||||
namespace YhCore.WebApi.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// SSO 路径保护中间件(对应 Java yh-auth-spring-boot-starter 的 /api/* 保护行为)。
|
||||
/// 规则(对齐 yh:sso 配置节):
|
||||
/// - yh:sso:enabled=false(默认):所有路径匿名放行,不影响开发联调;
|
||||
/// - yh:sso:enabled=true:匹配 /api/* / /mobiapi/* / 根路径(/, 浏览器 SSO 入口) 且未认证的请求
|
||||
/// → Challenge → Yh.Platform.Auth CasAuthenticationHandler 302 重定向到 CAS 登录页;
|
||||
/// - CAS 回调路径(yh:sso:signInCallbackPath,默认 /signin-cas)未认证时同样 Challenge:
|
||||
/// 直接访问回调(无 ticket)进入登录流程;携带失效/已消费 ticket 的回调则返回 401 错误页,
|
||||
/// 不再次 Challenge——避免“回调→登录→新票据→再失败”无限重定向循环(自动登录型 CAS 下必现);
|
||||
/// 回调校验成功(已认证 + 302)则短路管道,防止 MVC 端点匹配 404 覆盖 handler 的回跳 302;
|
||||
/// - Swagger 路径始终放行,方便本地接口调试;
|
||||
/// - /oapi 开放渠道始终放行:其准入由 AppAuthMiddleware 签名验签负责(X-App-Key / X-Timestamp /
|
||||
/// X-Signature 票据),无需用户登录态,SSO 登录保护永不覆盖该渠道(即使 yh:sso:urlPattern 误配
|
||||
/// /oapi/* 也不拦截),保证应用间调用不受浏览器登录流程影响。
|
||||
/// 调用方:Startup.Configure 中 UseAuthentication + UseYhUserContext 之后、UseAuthorization 之前。
|
||||
/// </summary>
|
||||
public class SsoProtectionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public SsoProtectionMiddleware(RequestDelegate next, IConfiguration configuration)
|
||||
{
|
||||
_next = next;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// 已认证(含 Cookie 恢复登录态)直接放行
|
||||
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"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// oapi 开放渠道:无需用户登录,准入由 AppAuthMiddleware 签名验签负责(票据 = 签名头),
|
||||
// 未带票据的请求由 AppAuthMiddleware 返回 401(而非 SSO 302),保证直接访问语义。
|
||||
if (isOapiPath)
|
||||
{
|
||||
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)))
|
||||
{
|
||||
// 请求携带 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(
|
||||
"<!DOCTYPE html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>登录失效</title></head>" +
|
||||
"<body style=\"font-family:system-ui;text-align:center;padding-top:80px\">" +
|
||||
"<h2>登录票据校验失败或已过期</h2>" +
|
||||
"<p>CAS 登录票据为一次性凭证,刷新或重复使用同一地址会导致校验失败。</p>" +
|
||||
"<p><a href=\"/\">重新登录</a></p></body></html>");
|
||||
return;
|
||||
}
|
||||
// 未认证 → Challenge → 302 重定向 CAS 登录页(ticket 校验与回跳由新框架 CasAuthenticationHandler 处理)
|
||||
await context.ChallengeAsync(YhAuthDefaults.CasScheme);
|
||||
return;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
/// <summary>路径是否命中保护模式(yh:sso:urlPattern / yh:sso:mobiApiPattern,默认 /api/*、/mobiapi/*,通配后缀匹配,忽略大小写)</summary>
|
||||
private static bool IsProtectedPath(string path, IConfigurationSection sso)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var urlPattern = sso["urlPattern"] ?? "/api/*";
|
||||
var mobiPattern = sso["mobiApiPattern"] ?? "/mobiapi/*";
|
||||
return MatchesPattern(path, urlPattern) || MatchesPattern(path, mobiPattern);
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string path, string pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 形如 /api/* → 前缀匹配;否则精确匹配(忽略大小写)
|
||||
if (pattern.EndsWith("/*", StringComparison.Ordinal))
|
||||
{
|
||||
var prefix = pattern.Substring(0, pattern.Length - 1); // 去掉 *
|
||||
return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return path.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user