服务器集成 LDAP 认证(Nginx)

为什么需要 LDAP 认证

我想,要回答这个问题,还是需要从企业管理的角度来着手。一个面向用户(2C)的产品,其用户基本上是不受地域因素限制的,而对于一个面向企业(2B)的产品,其用户基本上是在一个层次分明、有着明显边界的范围内。运营一个企业,除了业务系统以外,可能还需要 OA、财务、ERP 等等外围软件的支持,如果是一家互联网公司,可能还需要 DevOps、监控、协作等等方面的支撑。此时,从企业的角度自然是希望可以统一账号体系,这样就衍生出了各种各样的单点登陆和认证方案,单单是博主接触过的就有:OAuth2CASKeycloakIdentityServer4,这些方案可以说是各有千秋,此中曲折我们按下不表。

运行在 Windows Server 上的 AD

这里博主想说的是,一旦企业通过 AD Domain 或者说 Active Directory 来管理用户,就自然而然地牵扯出域登录或者域账号登录的问题。这类围绕 AD Domain 或者说域的问题,我们都可以考虑使用 LDAP 认证或者 Kerberos 认证,特别是后者,主流的软件如 Kafka、Zookeeper、MySQL 等等均支持这一协议,它可以实现在登录本地账户后,免登录打开一个网站的效果。可想而知,这是一个对企业而言极具诱惑力的特性,一个账号打通所有基础设施。当然,我承认 Kerberos 这个协议是非常复杂的,绝非三言两语可以厘清其中的千丝万缕,所以,我们今天只是聊聊 LDAP 认证这个话题。

通过 LDAP Browser 访问 AD

可能大家会纠结,LDAP 和 Active Directory 这两者间的关系,事实上, LDAP 是指轻量目录访问协议(Lightweight Directory Access Protocol),而 Active Directory 则是微软针对该协议的一种实现。当然,微软为了解决域控的问题,利用 LDAP 存储了一部分私有的数据。所以,两者的关系就像是接口和实现类,我们这里只需要 Active Directory 当成一台 LDAP 服务器即可。关于 Active Directory 的基础知识,这里不再做更多的科普。总而言之,通过 LDAP 我们可以对某个网站实现认证,从而达到保护资源的目的。譬如博主目前参与的前端项目,它是没有常规的登录、注册页面的,它采用的就是域账号登录的形式。下面,我们来看看如何集成 LDAP 认证。

如何集成 LDAP 认证

结合博主在上文中描述的场景,假设我们有一个前端项目通过 Nginx 或者 Apache 进行托管。通常情况下,我们可以直接访问这些前端页面,这意味这些资源是不受任何保护的。对企业来说,它更希望将这些资源保护起来,以确保只有它的员工或者说加入域的用户才可以访问,此时,我们该如何解决这个问题呢?

Nginx 篇

以 Nginx 为例,我们可以通过 nginx-auth-ldap 这个模块来解决这个问题。下面是一个简单的示意图,其基本思路是:在输入用户名和密码后,该模块会连接 LDAP 服务器对用户身份进行校验,如果校验通过,则会以 Basic 认证的的方式生成 Authorization 请求头;反之,将会返回 401 状态码,表示认证失败。

Nginx 集成 LDAP 示意图

注意到,这是一个第三方的模块,不管你是通过 Docker 或者主机来部署 Nginx,它都不会包含这个模块,此时,我们就需要为 Nginx 安装这个模块。因为 Nginx 采用的是静态编译的策略,所以,安装模块本质上就是同时拉取 Nginx 和模块的源码,然后重新编译生成二进制文件的过程。首先,我们来下载该模块的源码:

git clone https://github.com/kvspb/nginx-auth-ldap.git /usr/src/nginx-auth-ldap/; 

接下来,我们需要下载 Nginx 源码,无论你是在全新安装的 Nginx 上安装模块,还是在一个安装好的 Nginx 上安装模块,这一步都是必须的,千言万语汇成一句话:Nginx 采用的是静态编译的策略。博主这里是在 nginx:stable-alpine 这个镜像的基础上安装模块:

wget http://nginx.org/download/nginx-1.22.1.tar.gz
tar -zxvf nginx-1.22.1.tar.gz
cd nginx-1.22.1

接下来,如果是全新安装 Nginx,那么,你可以像下面这样列出常用的模块,然后用 --add-module 参数指向当前模块的路径:/usr/src/nginx-auth-ldap/ 。请注意,下面的参数经过简化,并不代表实际使用的参数:

./configure \
  --prefix=/etc/nginx \
  --sbin-path=/usr/sbin/nginx \
  --with-http_addition_module \
  --with-http_auth_request_module \
  --add-module=/usr/src/nginx-auth-ldap/ 

make && make install 

如果是安装好的 Nginx,那么,这个参数就必须从当前的 Nginx 上继承过来,否则,已经安装好的模块将不会参与编译。原本博主非常喜欢 Nginx ,可这一条博主实在不能容忍,这完全是在用户面前摆烂,难道使用了哪些了模块你心里没点数吗?非要逼着用户去帮你维护这些配置信息?这些又臭又长的参数真的不考虑做持久化吗?吐槽完 Nginx,我们继续来填坑,为了获取 Nginx 当前的配置参数,你可以使用 nginx -V 这个命令(注意:这个 V 必须要大写),此时,我们就可以得到下面的答案:

获取 Nginx 配置参数

这意味着什么呢?这意味着你需要把红框里的这一堆参数放到 ./configure 后面,然后再像上面一样添加 --add-module 参数,这简直是造孽啊!为了简化这个过程,我改进了一下脚本:

# 匹配 configure arguments ,然后再从第 21 个字符开始截取,即冒号后面的所有内容
NGINX_CONFIG=$(nginx -V 2>&1 | grep 'configure arguments' | cut -c21-)
./configure $NGINX_CONFIG --add-module=/usr/src/nginx-auth-ldap/ 
make && make install

当然,如果你亲自折腾过这一切,就知道这是痴心妄想,因为提取出来的这组参数会提示各种各样的错误,总之,你需要按照实际的情况来对这组参数就行调整,看着 Dockerfile 里比裹脚布还要长的参数,Nginx 官方难道你们不会心痛吗?当然,当你熬过这一切以后,我们就可以着手 Nginx 的配置啦:

http {
  # 定义 LDAP 服务器
  ldap_server ldap {
    # LDAP 服务器地址,使用 sAMAccountName 还是 uid 以实际的 AD 配置为准
    url ldap://<IP>:389/OU=OPS,DC=company,DC=com?sAMAccountName?one;
    # 管理员账号, CN/OU/DC 以实际的 AD 配置为准
    binddn "CN=<Administrator>,OU=OPS,DC=company,DC=com";
    # 管理员密码
    binddn_passwd "<Password>";
    group_attribute uniquemember;
    group_attribute_is_dn on;
    require valid_user;
  }

  server {
    listen  80;
    server_name  localhost;

    location / {
      # 启用 LDAP 认证
      auth_ldap "Forbidden";
      auth_ldap_servers ldap;
      root /usr/nginx/wwwroot;
      index index.html;
    }
  }
}

可以注意到,我们只需要按实际域控来配置 ldap_server 节点,然后为受保护的资源启用 LDAP 认证即可。此时,如果访问前端页面,浏览器将会提示输入用户名和密码:

Nginx 集成 LDAP 效果演示-1

如果我们输入的用户名或者密码不正确会怎么样呢?浏览器将会孜孜不倦地提示你输入用户名和密码。如果我们点击取消会怎么样呢?此时,如下图所示,它将会返回 401 状态码,表示认证失败:

Nginx 集成 LDAP 效果演示-2

此外,我们可以注意到,一旦认证通过,我们就可以正常访问前端页面。与此同时,所有的请求都会自动带上 Authorization 请求头,显然,这是一个 Basic 认证:

Nginx 集成 LDAP 效果演示-3

Nginx 集成 LDAP 效果演示-5

至此,我们就实现了 Nginx 下的 LDAP 认证集成。

Apache 篇

虽然 Nginx 比 Apache 更轻量、社区更活跃,可是在模块管理这方面,Apache 是完全吊打 Nginx 的,类似地,Apache 通过 mod-authnz-ldap 这个模块来实现 LDAP 认证的集成,并且这个模块有二进制包,可以直接通过包管理器来安装,从这方面来看,Nginx 完全不如 Apache :

# 安装模块
apt-get update;
apt-get install -y libldap2 mod_ldap mod-authnz-ldap; 
# 启用模块
a2enmod ldap; 
a2enmod authnz_ldap; 

接下来,我们只需要修改一下 Apache 的配置文件即可:

<VirtualHost *:80>
  ServerName localhost

  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/html

  LogLevel debug

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined

  <Location "/">
    AuthType Basic
    AuthName "LDAP SSO"
    AuthBasicProvider ldap
    AuthLDAPBindDN "CN=<Administrator>,OU=OPS,DC=company,DC=com"
    AuthLDAPBindPassword "<Password>"
    AuthLDAPURL ldap://<IP>:389/OU=OPS,DC=company,DC=com?sAMAccountName?one
    Require valid-user
  </Location>
</VirtualHost>

可以注意到,两者的配置是非常相似,可以实现相同的效果,这里就不再详细展示啦!

Backend 篇

在前面的示意图里,我们看到还有 Backend 这样一个环节。其实,很多时候,我觉得企业级应用的开发非常别扭,因为它总是在现代中透着些古板,说它现代是因为它在跟进微服务、前后端分离、容器化这些流行趋势,说它古板是因为它总在尝试用新技术做旧时代的东西。譬如,前后端分离以后,JWT 是最常见的认证方式,在这种模式下,服务就应该是无状态的,可实际开发中它还是会搞出来一个 Session 的概念,难道现在还是汤姆猫的时代吗?这个域账号登录的想法确实不错,可它和现在主流的 JWT 是不兼容的,除非在服务器端实现了 Basic 认证。下面是 ASP.NET Core 中实现 Basic 认证的一个简单流程:

async Task Invoke(HttpContext context)
{
    try
    {
        // 1.提取用户信息
        var header = context.Request.Headers["Authorization"]
        var authHeader = AuthenticationHeaderValue.Parse(header);
        var authParams = Convert.FromBase64String(authHeader.Parameter);
        var credentials = Encoding.UTF8.GetString(authParams).Split(':', 2);
        var username = credentials[0];
        var password = credentials[1];
        
        // 2.验证用户信息
        var user = await _userService.Authenticate(username, password);
        if (user != null)
        {
            // 3.指定当前用户
            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.UserName),
            };
            var identity = new ClaimsIdentity(claims, "Basic");
            var principal = new ClaimsPrincipal(identity);
            context.User = principal;
        }
    }
    catch {}

    await _next(context);
}

可以看到,Basic 认证的过程其实就是从 Authorization 请求头里提取用户信息并进行验证的过程,博主这里是以中间件的形式来进行说明。按照一般的做法,你可能需要继承 AuthenticationHandler 并重写 HandleAuthenticateAsync() 和 HandleChallengeAsync() 这两个方法,思路其实是完全一样的,这里就不再详细展开说啦!总而言之,如果前端想结合 LDAP 做身份认证,需要后端提供相应的支持。当然,如果你可以在请求传入到后端前自动产生一个 JWT 令牌,那再好不过啦!

转载自飞鸿踏雪

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值