wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 + "/"));
    }
}