92 lines
6.2 KiB
C#
92 lines
6.2 KiB
C#
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=<urlencoded>]——回跳走 query 而非平台旧版
|
||
/// ;redirectToReferer=<hex> 矩阵参数(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;
|
||
}
|
||
}
|
||
}
|