IIS自定义HTTP模块反向代理的Windows Authentication异常排查
解决自定义IIS代理的Windows Authentication(NTLM)异常问题
根据你描述的这个NTLM认证异常场景——直接访问目标站正常,代理访问时仅前几个请求认证通过,后续静态资源反复触发登录弹窗,结合你已经做的排查,核心问题大概率和NTLM的连接绑定特性以及HTTP 1.1 Keep-Alive头的传递/连接复用有关,毕竟ARR代理之所以正常,就是它默认处理了这些细节。下面给你具体的排查和修复方向:
1. 先修复Keep-Alive头的传递与连接复用问题
NTLM认证是和TCP连接强绑定的:一旦某个连接完成NTLM握手,后续通过该连接发送的请求会自动复用认证上下文,无需重新认证。如果你的代理没有传递Connection: Keep-Alive头,或者每次转发请求都新建TCP连接,那后续静态资源请求就会因为用了新连接而触发重新认证。
具体修复步骤:
- 在自定义代理中传递原始请求的Connection头:
如果你用的是HttpWebRequest转发请求,需要手动复制原始请求的Connection头到转发请求中,同时确保启用Keep-Alive(默认是开启的,但保险起见显式设置):// 构建转发请求时 var forwardRequest = (HttpWebRequest)WebRequest.Create(targetUrl); // 复制原始请求的Connection头(包括Keep-Alive) if (HttpContext.Current.Request.Headers["Connection"] != null) { forwardRequest.Connection = HttpContext.Current.Request.Headers["Connection"]; } // 显式启用Keep-Alive forwardRequest.KeepAlive = true; - 改用HttpClient实现连接池:
HttpWebRequest的连接复用机制比较弱,建议换成全局单例的HttpClient,它默认会维护连接池,自动复用Keep-Alive连接:
注意:HttpClient必须是全局单例,否则会导致连接泄露,反而破坏Keep-Alive的复用逻辑。// 全局单例(必须在应用启动时初始化,不要每次请求都new) private static readonly HttpClient _proxyHttpClient = new HttpClient(); // 在转发请求的逻辑中 public async Task ForwardRequest(HttpContext context, string targetUrl) { var requestMsg = new HttpRequestMessage( new HttpMethod(context.Request.HttpMethod), targetUrl ); // 复制原始请求的所有头(包括Authorization、Connection等) foreach (var header in context.Request.Headers) { if (!requestMsg.Headers.TryAddWithoutValidation(header.Key, header.Value)) { requestMsg.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value); } } // 发送请求并回写响应 var response = await _proxyHttpClient.SendAsync(requestMsg); context.Response.StatusCode = (int)response.StatusCode; foreach (var header in response.Headers) { context.Response.Headers[header.Key] = string.Join(", ", header.Value); } foreach (var header in response.Content.Headers) { context.Response.Headers[header.Key] = string.Join(", ", header.Value); } await response.Content.CopyToAsync(context.Response.OutputStream); }
2. 处理NTLM的请求顺序限制
NTLM不允许同一TCP连接上的并行请求——同一连接上的请求必须按顺序完成处理。如果你的代理同时并行转发多个静态资源请求到目标服务器的同一个连接,就会导致认证上下文混乱,出现401重复触发的情况。
具体处理:
- 不要用简单的
SyncLock做全局串行,这会严重影响性能。可以按客户端会话(比如客户端IP+UserAgent,或者ASP.NET SessionID)做分组,同一组的请求串行处理,不同组的请求并行:
这样既保证了同一客户端的请求按顺序处理,又不会影响其他客户端的并发。// 用字典存储每个会话的锁对象 private static readonly Dictionary<string, object> _sessionLocks = new Dictionary<string, object>(); private static readonly object _lockObj = new object(); // 获取会话锁 private object GetSessionLock(HttpContext context) { var sessionKey = $"{context.Request.UserHostAddress}_{context.Request.UserAgent}"; lock (_lockObj) { if (!_sessionLocks.TryGetValue(sessionKey, out var sessionLock)) { sessionLock = new object(); _sessionLocks[sessionKey] = sessionLock; } return sessionLock; } } // 在BeginRequest中使用会话锁 public void BeginRequest(object sender, EventArgs e) { var context = ((HttpApplication)sender).Context; var sessionLock = GetSessionLock(context); lock (sessionLock) { // 执行转发逻辑 ForwardRequest(context, targetUrl).Wait(); } }
3. 验证排查点
用Fiddler抓代理到目标服务器的请求,确认两个关键点:
- 转发的请求中是否包含
Connection: Keep-Alive头; - 后续静态资源请求的TCP连接ID(Fiddler的
ClientPort列)是否和第一个成功认证的请求一致——如果一致,说明连接复用成功;如果不一致,说明连接没有被复用,需要检查代理的连接池配置。
最后补充
你提到手动逐个放行请求就正常,这正好验证了“请求顺序+连接复用”的问题:逐个放行时,每个请求都在同一个连接上完成了完整的认证握手,后续请求复用了该连接的认证上下文。而正常并行请求时,多个请求抢占同一个连接,破坏了NTLM的连接绑定逻辑。
内容的提问来源于stack exchange,提问作者user3663873
相关产品推荐
相关产品推荐

