using BoardApp.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace BoardApp.Controllers; /// /// Windows 一键登录。 /// Kestrel + Negotiate:首次请求通常无 Authorization,必须先 返回 401+WWW-Authenticate, /// 浏览器才会带 SPNEGO/NTLM 重试;否则 AuthenticateAsync 永远失败,误报「未携带 Windows 集成身份」。 /// 已带 Negotiate/NTLM 头仍无主体时,不再 Challenge,避免死循环;用一次性 Cookie 防止非域环境反复 401。 /// public sealed class NegotiateAuthController : ControllerBase { private const string NegotiateTryCookie = "board.negotiate_try"; private readonly AdAuthenticationService _adAuth; public NegotiateAuthController(AdAuthenticationService adAuth) { _adAuth = adAuth; } private string BasePath => Request.PathBase.HasValue ? Request.PathBase.Value! : "/board"; [HttpGet("/api/winauth/negotiate")] [AllowAnonymous] public async Task NegotiateLogin([FromQuery] string? next) { var loginPrincipal = await AdAuthenticationService.ResolveWindowsPrincipalAsync(HttpContext, User); if (loginPrincipal == null) { var authHeader = Request.Headers.Authorization.ToString(); var hasSpnego = authHeader.StartsWith("Negotiate ", StringComparison.OrdinalIgnoreCase) || authHeader.StartsWith("NTLM ", StringComparison.OrdinalIgnoreCase); var alreadyChallenged = Request.Cookies[NegotiateTryCookie] == "1"; // 尚无浏览器票据:触发集成认证握手(域内常见为静默重试,不经过本页的 Redirect 文案) if (!hasSpnego && !alreadyChallenged) { Response.Cookies.Append(NegotiateTryCookie, "1", new CookieOptions { HttpOnly = true, IsEssential = true, Path = "/", MaxAge = TimeSpan.FromMinutes(2), SameSite = SameSiteMode.Lax, Secure = Request.IsHttps }); return Challenge(); } Response.Cookies.Delete(NegotiateTryCookie, new CookieOptions { Path = "/" }); var msg = hasSpnego ? "Windows 集成登录未能验证身份(票据无效或非域环境),请使用账号密码登录。" : "当前环境无法完成 Windows 集成登录,请使用账号密码登录。"; return Redirect($"{BasePath}/login?negotiate_error={Uri.EscapeDataString(msg)}"); } Response.Cookies.Delete(NegotiateTryCookie, new CookieOptions { Path = "/" }); var result = await _adAuth.AdLoginAsync(HttpContext, loginPrincipal, next, BasePath); if (!result.Ok) { return Redirect($"{BasePath}/login?negotiate_error={Uri.EscapeDataString(result.Error ?? "登录失败")}"); } return Redirect(result.Redirect ?? (BasePath + "/")); } }