Files
DocprintService/src/YhCore.WebApi/Auth/Cas/YhCasAuthServiceCollectionExtensions.cs
T
wanghu f711f1c5e5
build / build-push (push) Successful in 1m23s
s
2026-09-07 15:47:20 +08:00

92 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace YhCore.WebApi.Auth
{
/// <summary>
/// 本地化 CAS 认证装配(对齐 Yh.Platform.Auth 的 AddYhAuth 认证部分;service 构造为
/// {root}{SignInCallbackPath}[?returnUrl=&lt;urlencoded&gt;]——回跳走 query 而非平台旧版
/// ;redirectToReferer=&lt;hex&gt; 矩阵参数(ASP.NET Core 不支持矩阵参数,见 CasAuthenticationHandler)。
/// 不含 AddYhOpenApi,OpenApi/AppAuth 由宿主另行注册)。
/// 配置节:<c>yh:auth</c>(通用)与 <c>yh:sso</c>(CAS 服务端)。
/// </summary>
public static class YhCasAuthServiceCollectionExtensions
{
public static IServiceCollection AddYhCasAuth(this IServiceCollection services, IConfiguration configuration)
{
// 会话票据表单例:Cookie TicketDataFormat 与 OnSigningOut 退出吊销必须共享同一实例(同一票据表)
var sessionTicketFormat = new SessionTicketDataFormat();
services.AddSingleton(sessionTicketFormat);
services.AddOptions<YhAuthOptions>().Bind(configuration.GetSection(YhAuthOptions.SectionName));
// 注:AddScheme 的 configure 才是 handler 实际读取的命名 options(AuthenticationHandler<TOptions>.Options);
// 必须在此绑定 yh:sso,否则 handler 拿到的 CasAuthenticationOptions 为空(Challenge 时 CasServerUrlPrefix 缺失)。
services.AddOptions<CasAuthenticationOptions>().Bind(configuration.GetSection("yh:sso"));
services.AddAuthentication(options =>
{
// DefaultScheme 必须为 CasScheme(而非 CookieScheme):
// UseAuthentication 每请求调用 AuthenticateAsync(默认 scheme),若默认是 CookieScheme,
// CAS 票据校验(CasAuthenticationHandler.HandleAuthenticateAsync 的 ticket 分支)永远没有触发点——
// 回调带 ticket 时认证不校验、SsoProtectionMiddleware 见未认证直接 401,CAS 登录永远失败。
// CasAuthenticationHandler.HandleAuthenticateAsync 第一步就是 Context.AuthenticateAsync(CookieScheme)
// 恢复 Cookie 登录态,因此 CasScheme 作为默认认证 scheme 语义等价"先 Cookie 恢复、后 ticket 校验"。
options.DefaultScheme = YhCasDefaults.CasScheme;
options.DefaultChallengeScheme = YhCasDefaults.CasScheme;
})
.AddCookie(YhCasDefaults.CookieScheme, options =>
{
var auth = configuration.GetSection(YhAuthOptions.SectionName).Get<YhAuthOptions>() ?? new YhAuthOptions();
options.Cookie.Name = auth.CookieName;
options.Cookie.HttpOnly = true;
// SameSite=Lax:登录/回调全部是顶层导航,Lax 足够。
// 【勿改 None】HTTP 部署下无 Secure 属性的 SameSite=None Cookie 会被浏览器直接丢弃
// (Chrome/Edge 强制 None 必须 Secure),导致登录 Cookie 永远种不上 → 无限重定向循环
// (2026-09-07 线上复现:验票成功但下一请求无 Cookie → ERR_TOO_MANY_REDIRECTS)。
// 跨 host 登录态需求由 DebugUser 头方案解决(见 docs/本地前端联调线上后端SSO方案.md)。
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
// 会话票据:TicketDataFormat 把票据存服务端,Cookie 只写 32 位 key;
// IsPersistent=false(CasAuthenticationHandler)会话 Cookie,关闭浏览器即失效;
// SlidingExpiration=false 固定过期,不滑动续期。
options.TicketDataFormat = sessionTicketFormat;
options.ExpireTimeSpan = TimeSpan.FromMinutes(auth.CookieExpireMinutes > 0 ? auth.CookieExpireMinutes : 1);
options.SlidingExpiration = false;
options.LoginPath = "/api/base/GetUserInfo";
// 仅 Cookie 自身 Challenge(未登录访问需授权页 → 跳 LoginPath)时使用;实际登录走 CasScheme,
// 参数名须与 CasAuthenticationHandler.ReturnUrlParam("returnUrl") 一致,避免双参数名并存误导。
options.ReturnUrlParameter = "returnUrl";
// 退出即吊销:SignOutAsync(如业务 GET /api/Logout)清除 Cookie 时,
// 从请求 Cookie 取 32 位 key 立即删除服务端票据——否则票据残留到 ExpireTimeSpan 过期才失效。
options.Events = new CookieAuthenticationEvents
{
OnSigningOut = context =>
{
if (context.HttpContext.Request.Cookies.TryGetValue(auth.CookieName, out var rawKey))
{
sessionTicketFormat.Revoke(rawKey);
}
return Task.CompletedTask;
},
};
})
.AddScheme<CasAuthenticationOptions, CasAuthenticationHandler>(YhCasDefaults.CasScheme,
o => configuration.GetSection("yh:sso").Bind(o));
services.AddSingleton<CasServiceTicketValidator>(sp =>
{
var opts = sp.GetRequiredService<IOptionsMonitor<CasAuthenticationOptions>>().CurrentValue;
return new CasServiceTicketValidator(opts.CasServerUrlPrefix, opts.TicketValidateTimeoutMs);
});
// 用户明细 SPI 默认实现(调 UserBase /oapi/user/byLoginName),UserContextMiddleware 依赖
services.AddScoped<Yh.Platform.Auth.IUserDetailsProvider, Yh.Platform.Auth.OpenApiUserDetailsProvider>();
return services;
}
}
}