1
build / build-push (push) Successful in 1m14s

This commit is contained in:
wanghu
2026-09-04 09:58:52 +08:00
parent b7789a1914
commit 9fe45c810e
5 changed files with 532 additions and 0 deletions
@@ -0,0 +1,86 @@
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;
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;
}
}
}