问题背景

 前段时间,负责ELK那哥们儿想把Kibana调整成为LDAP内部用户认证,运维这边了解到这个需求,着手调研。

解决方案及思路

 首先Kibana现在只是用了一个叫Search guard的插件来控制访问,但是这个插件需要和JIRA用户统一认证,需要手动开通账户,维护成本高,并且实施起来的效率也很低,所以才打算搭建一个LDAP服务器,然后同步JIRA用户信息,实现Kibana统一认证。  经过讨论方案,大致的初步方案就是通过结合Nginx的auth_request模块,当用户请求一个受特定保护的资源时候,auth_request会将请求转发到LDAP验证服务上,然后根据验证服务返回的状态码决定重定向或者允许访问等进一步的操作。其中在Nginx配置文件中的LDAP认证服务中需要完善LDAP服务器的配置信息。大概的思路就是这样,想到这里的时候,又发现一个问题,如何保证LDAP和JIRA用户能够及时同步呢,并且各种权限的信息也需要相应的对接。  这时候负责JIRA的哥们儿,说其实我们那边有个接口,可以通过POST用户名和密码信息,验证是否能够登录。并且验证通过会返回一个有关用户信息的JSON,如果想要进行权限控制的话,也可以通过这个JSON。所以现在事情就变得简单了,重新简单梳理一下解决方案。  如下:

  • 1、用户请求受保护的Nginx反向代理资源
  • 2、Nginx的auth_request模块将请求转发给自己写的django的验证服务中。判断cookie解密后是否正确。如果都符合条件则返回200状态码,nginx不会拦截请求,而是构建一个subprocess请求受保护的本地资源。
  • 3、如果django验证服务没有通过,则会返回一个401请求,nginx对401请求进行拦截并且重定向到登录页面,所以就返回到django的login服务中,展示登录界面。
  • 4、在登录界面中,输入用户名和密码提交,传到后端时,后端使用内部接口,发送用户名和密码,通过返回的状态码验证是否成功,成功之后将用户名加密之后放入cookie并且重定向到受保护的资源中;验证失败则返回提示用户名或者密码失败。

详细设计

前期准备工作

版本选择: Nginx 1.14.1 Django 1.9.13 Kibana 6.5.1 Python 2.7

需要注意事项:
1、Nginx需要注意的点就是,默认yum安装的Nginx没有编译auth_request模块,
所以需要到官网重新下载源码,增加--with-http_auth_request_module进行编译。
2、代码中需要用到Python的requests、Crypto 模块需要额外安装

Nginx 1.4.1下载地址: http://nginx.org/download/nginx-1.14.1.tar.gz requests 模块安装: pip install requests Crypto模块安装: pip install Crypto

** 安装成功后,可分别通过nginx -V、pip show requests、pip show Crypto验证:**

代码分析

 最开始设计后端的验证服务时参考了Nginx官网上,Nginx结合ldap的身份验证demo,demo地址:  https://github.com/nginxinc/nginx-ldap-auth  demo中大致的思路就是接受请求,首次访问将会重定向到login页面上,loging登录后,将接收到的用户名和密码,去查询ldap服务器上的信息,成功后将把用户名和密码以 ; 号连接后做 base64 写入 cookie,下一次访问受保护的资源时,然后写 Location 里写入 target 的值,来实现重定向跳回。  但是demo中首先的问题就是用户和密码这种敏感的信息不应该放入cookie中,并且在demo中用到了简单的BaseHTTPServer ,用这个模块就会出现几个问题:

  • 1、不支持url的解析和转发,需要用户自己解析
  • 2、回写的响应需要自己维护格式,容易出错
  • 3、没有模板支持,如果需要写HTML页面,也需要自己维护。  ** 所以为了解决以上的缺点,刚好对于Django有一定的了解,就打算基于Django框架实现用户验证的服务,以及登录时模板页面。废话不多说来看看代码。**

代码详解:首先是验证服务,通过检查是否存在cookie,不存在的话就返回状态码401;如果存在的话,通过将cookie解密,获取其中关键的字段,判断是否登录,如果解密成功的话,返回状态码200,解密失败的话,返回状态码401

class prcrypt(object):
    def __init__(self,key):
        if len(key) < 16:
            key = key+(16-len(key))*'\0'
        self.key = key
        self.mode = AES.MODE_CBC

    def encrypt(self,text):
        cryptor = AES.new(self.key,self.mode,IV=self.key)
        length = 16
        count = len(text)
        add = count % length
        if add :
            text = text + ('\0' * (length-add))
        self.ciphertext = cryptor.encrypt(text)
        return base64.b64encode(self.ciphertext)

    def decrypt(self,text):
        cryptor = AES.new(self.key,self.mode,IV=self.key)
        plain_text = cryptor.decrypt(base64.b64decode(text))
        return plain_text.rstrip('\0')
		```
	

>  **代码详解: **这段加密操作通过 **aes **模块进行加密,大概原理首先传入一个 **key **值,作为实例化 **AES对象的密钥 **。然后将需要加密的文本补足成 **16的倍数 **进行加密操作,最后将加密后的文本 **base64 **进行转换。解密的话 **逆操作 **即可。

![](https://s1.51cto.com/images/blog/201812/17/cd622f3580fbf651dfb76a50c1995d6e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)

> **代码详解:**首先**前面一部分**代码是接收前端传来的用户名和密码,并且通过requests模块post到指定的接口地址,然后解析返回的结果,根据返回结果的状态码判断是否能登陆成功。
> **第二部分**通过判断登录成功后,将**用户名和登录状态**加密,设置cookie,指定**超时时间**一小时,只能由http协议传输。然后重定向到**受保护的资源**,受保护的资源就会再次请求**验证服务**,验证服务对cookie进行**判断正确后**就会允许访问受保护资源;如果**登录失败**则会返回无效用户名和密码到登录界面。

**前端登录界面:**
![](https://s1.51cto.com/images/blog/201812/17/a4e110a86e749b6a86648747cca0ce02.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
### 配置分析

http { include /etc/nginx/mime.types; upstream backend { server 192.168.128.5:5601; } #需要受保护的Kibana程序

server {
    listen 8081;
    # nginx服务开放8081端口

    location / {
        auth_request /auth-proxy;
        error_page 401 =200 /login;
        proxy_pass http://backend/;
    }
    #这个路径下受auth-request保护,所有401的请求都会重定向到login上

    location /login {
        proxy_pass http://127.0.0.1:8888/login;
        proxy_set_header X-Target $request_uri;
    }
   # 这是我们认证的页面,指向我们django项目中的login路径

    location /auth-proxy {
        internal;
        proxy_pass http://127.0.0.1:8888;
    }
   # 这里用作auth-request请求的路径

    location /static{
    alias /data/htdocs/www/elk/static/;
    }
}

}

> 上面就是主要的Nginx配置文件

urlpatterns = [ url(r'^$',login), url(r'^login',login), url(r'^auth-proxy',nginx_auth), url(r'^admin/', include(admin.site.urls)), ]

> 对应django项目的urls文件

## 总结
> 其实这个用户认证的解决方案不难理解,比较麻烦就是最初需求了解确定以及和其他项目负责人沟通的问题。在技术上单纯的没什么难点。
> 通过在网上查资料,还有Nginx官网上的一些demo有助于你迅速了解到模块的用处,以及对一些问题的解决方案,不少网友也遇到了类似的问题,在他们身上多总结经验,有助于自己解决问题,所以一句话“多动手,多沟通,知行合一”。
> 以下是我参考的一些网站和博客,大家有兴趣可以看看:
> Nginx官网上利用auth_request结合LDAP的认证方案:
 https://www.nginx.com/blog/nginx-plus-authenticate-users/

> 用 Nginx 的 auth_request 模块集成 LDAP 认证:
 https://www.jianshu.com/p/9f2da3cf5579
> python的aes加密和解密:
 https://my.oschina.net/u/1458120/blog/648350