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 + "/"));
}
}