using BoardApp.Services;
|
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Mvc;
|
|
namespace BoardApp.Controllers;
|
|
/// <summary>
|
/// Windows 一键登录。
|
/// Kestrel + Negotiate:首次请求通常无 Authorization,必须先 <see cref="ControllerBase.Challenge"/> 返回 401+WWW-Authenticate,
|
/// 浏览器才会带 SPNEGO/NTLM 重试;否则 <c>AuthenticateAsync</c> 永远失败,误报「未携带 Windows 集成身份」。
|
/// 已带 Negotiate/NTLM 头仍无主体时,不再 Challenge,避免死循环;用一次性 Cookie 防止非域环境反复 401。
|
/// </summary>
|
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<IActionResult> 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 + "/"));
|
}
|
}
|