如何让openresty支持ntlm认证

robots

 

背景

openresty 作为反向代理被广泛应用于各种项目中,微软的很多产品采用ntlm认证,当两者相遇时,会碰撞出令人头痛的问题——-认证失败,如果项目具有认证重试的功能,则会发生反复要求输入用户名,密码的现象,无法完成认证,后续功能也就无从谈起。openresty核心采用的是开源版nginx,没有对ntlm认证提供支持,nginx plus商业版虽提供了ntlm支持,但从成本角度考虑,为了这个 特性采购商业版却是不划算的,因此本文将讨论如何在openresty的基础上实现ntlm认证的支持。

 

NTLM over HTTP 认证简介

NTLM是一套身份验证和会话安全协议,在多种Microsoft网络协议实现中使用,基于http的ntlm认证流程如下图所示:

ntlm认证流程图

1.客户端向服务端请求资源;
2.服务端返回401状态码,要求客户端完成认证,并在消息头WWW-Authenticate中包含NTLM标识;
3客户端向服务端发送请求,Authorization消息头中携带Type1(协商) message,它主要包含客户端支持和服务器请求的功能列表,从此刻开始,客户端到服务端必须维持长连接,如果链接中断,需要重新认证,也就是说,如果无法维持在同一个tcp长连接上传输如下消息,则ntlm认证无法完成;
4 服务端返回401状态码,并在Authorization消息头中包含type2(质询) message,消息中将包含服务器产生的Challenge;
5客户端通过发送请求,在Authorization头中包含type3 message 以回复type2的质询,type3 message 中包含 用户密码hash加密过的challenge信息;
6.服务端使用用户密码hash加密challenge 并将加密结果与第5步客户端发来的加密信息对比,如相等则认证通过,返回资源;
认证流程中有一点比较关键,也是本文解决问题的核心点,即ntlm认证是 连接导向的( connector-oriented),完成ntlm认证之后,所有后续的http请求都需要保持在此连接上,如果连接中断了,服务端则会要求客户端重新认证。

 

定位问题

在我们的安全项目中,需要用openresty将客户端的请求代理到后端服务器,nginx的连接对应关系如下图所示,其中conn 为客户端与nginx建立的长连接,peer_conn为nginx与后端服务器创建的长连接:

nginx原始连接关系图

nginx与后端服务器之间创建了连接池,可以复用长连接,但每次客户端的请求转发至后端服务器时,nginx会从连接池中取出第一个可用的长连接,用完之后 连接又会放回连接池中,这就无法保证客户端和服务器一直使用相同的连接传输数据,当连接更换,客户端就需要和服务器重新完成认证流程,最糟糕的情况就是客户端和服务器无法完成ntlm认证。

 

解决方案

为支持ntlm认证,需要让开源nginx具备如下特性

改造后nginx连接对应关系

如图所示,为使客户端到业务服务端传输消息维持在同一个的tcp连接上,需要让conn 和peer_conn对应起来,即同一个conn上接受的请求都通过同一个peer_conn转发至业务服务端,这个过程中需要存储conn 和peer_conn的对应关系。本文中存储的是conn连接对应的socket套接字描述符client_fd 和 peer_conn连接对应的socket套接字描述符upstream_fd的对应关系,nginx底层提供了多种高级数据结构,其中hash无法动态增长,如要使用需在原数据结构上进行二次修改,成本较高,在易用性和效率上考虑radixTree很适合存储conn和peer_conn的对应关系,radixTree的详细信息感兴趣可以查看《nginx模块开发与框架解析》。接下来本文将从源码和nginx配置两方面介绍如何让开源nginx支持ntlm认证。

源码实现

源码修改主要包含如下3部分
1、ngx_http_upstream_connect 这个函数主要负责与upstream建立tcp连接,关键函数调用关系如下:
ngx_http_upstream_connect -> ngx_event_connect_peer -> ngx_http_upstream_get_keepalive_peer
ngx_http_upstream_connect 函数中首次获取到 upstream的tcp链接后,需要将client_fd和upstream_fd的相互对应关系存储到基数树conn_radix_tree中,此处需创建内存池对象,用于为conn_radix_tree提供内存资源,后面每次需要向upstream发送数据,都从conn_radix_tree中寻找client_fd 对应的upstream_fd。

2.ngx_event_connect_peer 代码中含有如下语句,rc = pc->get(pc, pc->data);get为函数指针,指向 ngx_http_upstream_get_keepalive_peer(ngx_peer_connection_t *pc, void *data)
此函数的主要功能是从后端服务器的连接池中获取第一个可用的长连接,并通过参数指针pc返回找到的长连接,原始代码如下图所示:

nginx原连接池取连接逻辑

为实现ntlm认证支持,需要添加如下判断逻辑:
a.nginx首次转发client请求时,未建立client_fd 与upstream_fd的映射关系,则需要从缓存中寻找一个未被其它 client 使用的tcp 长连接,转发client的请求;
b. 非首次转发client请求,则已经缓存client_fd 与upstream_fd的映射关系到conn_radix_tree,在遍历 连接池时,如果当前连接的套接字是 client_fd 映射的那个upstream_fd,则使用此tcp长连接转发client 请求;
修改后从nginx到后端的连接池中获取连接逻辑如下所示:

改造后从nginx到后端的连接池中获取连接

3.ngx_http_keepalive_handler 函数处理nginx与client客户端连接断开的情况,代码中在关闭client客户端连接的地方,删除conn_radix_tree中client_fd 和 upstream_fd对应的映射关系,代码如图:

关闭连接处附加删除连接对应关系

删除连接对应关系的代码

nginx 配置

配置主要涉及如下3个地方:
1.keepalive_timeout 60;客户端到nginx使用长连接,长连接有效期为60s
2.upstream{
ip:xxxxx;
keepalive 5000;
}
设置单个进程最大支持5000个到upstream的长连接
3.proxy_http_version 1.1;nginx默认使用1.0,只有配置为1.1 才支持到upstream的keepalive 长连接

 

总结

以上就是ntlm认证的主要原理和特性,openresty不支持ntlm认证的原因以及解决方案。目前此方案线上已稳定运行近一年,如果大家有遇到类似问题或相关需求,欢迎一起交流学习。

参考资料

1.《nginx模块开发与框架解析》
2. NTLM协议详细文档 https://curl.haxx.se/rfc/ntlm.html

(完)