предисловие
Много раз нам нужно добавить модуль аутентификации для некоторых предприятий, у которых нет функций аутентификации личности.
- Например, в бесплатной версии ELK на Kibana нет аутентификации личности;
- Например, Open-Falcon версии 0.1 не сертифицирован для Dashboard;
- Или некоторые веб-сайты, которые изначально были открыты для публики, вдруг перестали открываться для публики в определенные особые дни и в определенное особое время. . .
Обычно нелегко непосредственно изменять навязчивое решение бизнеса, а не навязчивое решение обычно может достичь аналогичных эффектов, таких как добавление агента к нему, а затем выполнение основной аутентификации HTTP.
Это хороший способ, но базовая HTTP-аутентификация все-таки слишком проста, и неудобно интегрировать внешние источники аутентификации, такие как LDAP.
Таким образом, более гибкое решение — модуль auth_request Nginx.
Модуль auth_request для Nginx
auth_request — это запрос конкретной службы при посещении пути, защищенного auth_reuqest в Nginx. В соответствии с кодом состояния, возвращаемым этой службой, модуль auth_request делает следующий шаг, разрешая доступ или перенаправление, чтобы отпрыгнуть или что-то в этом роде. Таким образом, мы можем настроить все наши индивидуальные потребности на нем.
Предполагая, что наша среда — Centos, установка nginx в yum опущена. Поскольку nginx, установленный через yum и т. д., по умолчанию не компилирует модуль auth_request. Нам нужно перекомпилировать.
беги первымnginx -V
чтобы получить текущийnginx
параметры компиляции
# nginx -V
nginx version: nginx/1.14.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'
Сначала установите некоторые зависимости
yum -y install gcc gcc-c++ autoconf automake make
yum -y install zlib zlib-devel openssl
yum -y install openssl-devel pcre pcre-devel
yum -y install libxslt-devel
yum -y install redhat-rpm-config
yum -y install gd-devel
yum -y install perl-devel perl-ExtUtils-Embed
yum -y install geoip-devel
yum -y install gperftools-devel
затем скачатьnginx
изисходный код, используя только что полученные параметры компиляции, добавляем--with-http_auth_request_module
перекомпилировать параметр
# ./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' --with-http_auth_request_module
# make
# make install
Сноваnginx -V
Проверьте это, у вас есть этоhttp_auth_request_module
охватывать
# nginx -V
nginx version: nginx/1.14.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' --with-http_auth_request_module
простая демонстрация
nginx.incДана очень простая демонстрация, а именноnginx-auth-ldapэто репо.
Вся логика показана на следующем рисунке
image.pngПодробная блок-схема примерно выглядит следующим образом:
image.png
-
Клиент отправляет HTTP-запрос, чтобы получить защищенный ресурс обратного прокси на Nginx.
-
Nginx
auth_request
модуль перенаправляет запрос наldap-auth
Этот сервис (соответствующий nginx-ldap-auth-daemon.py) обязательно выдаст ошибку 401 . -
Nginx перенаправляет запрос на
http:// backend / login
, который здесь соответствует серверной службе. будетuri
написатьX-Target
, так что вы можете прыгать позже. -
Серверная служба отправляет клиенту форму входа (форма
demo
определено в кодексе). в соответствии сerror_pageконфигурации, Nginx вернет код статуса http формы входа как 200. -
Пользователь заполняет поля имени пользователя и пароля в форме и нажимает кнопку входа в систему из
/ login
положить началоPOST
Запрос, Nginx перенаправляет его на серверную службу. -
Серверная служба записывает имя пользователя и пароль в файл cookie в формате base64.
-
Клиент повторно отправляет свой первоначальный запрос (из шага 1), теперь уже с файлом cookie. Nginx перенаправляет запрос на
ldap-auth
службы (как показано на шаге 2). -
ldap-auth
Служба декодирует файл cookie, а затем выполняет аутентификацию LDAP. -
Дальнейшее действие зависит от успеха аутентификации LDAP:
- Если аутентификация прошла успешно, то
ldap-auth
Служба возвращает код состояния 200 в Nginx. Nginx запрашивает ресурсы у серверных служб. существуетdemo
, серверная служба возвращает следующий текст:Hello, world! Requested URL: URL
- Если аутентификация не удалась,
ldap-auth
Служба возвращает 401 . Nginx снова перенаправляет запрос на серверную службу.Login
(как в шаге 3) и повторите процесс.
- Если аутентификация прошла успешно, то
Демонстрационный тест
Сначала установите зависимости
yum install python-ldap
Затем клонируйте репо
#git clone https://github.com/nginxinc/nginx-ldap-auth.git
# ls
backend-sample-app.py Dockerfile nginx-ldap-auth.conf nginx-ldap-auth-daemon-ctl.sh nginx-ldap-auth.default nginx-ldap-auth.service rpm
debian LICENSE nginx-ldap-auth-daemon-ctl-rh.sh nginx-ldap-auth-daemon.py nginx-ldap-auth.logrotate README.md
некоторые изnginx-ldap-auth.conf
Это пример конфигурации Nginx, просто скопируйте его напрямую
# cp nginx-ldap-auth.conf /etc/nginx/nginx.conf
Конфигурационный файл Nginx выглядит следующим образом, он упрощен и добавлены китайские комментарии.
error_log logs/error.log debug;
# 这里把日志放在 nginx 目录下,所以要么改掉要么在 nginx 目录下建个 log 目录
events { }
http {
# cache 路径和大小
proxy_cache_path cache/ keys_zone=auth_cache:10m;
# 将要被 nginx auth_request 保护的 backend
# 在这个 demo 里是 backend-sample-app.py.
upstream backend {
server 127.0.0.1:9000;
}
# nginx 服务起在 8081 上
server {
listen 8081;
# 这个路径被 auth_request 保护了, 401 重定向到 login 上
location / {
auth_request /auth-proxy;
# redirect 401 to login form
error_page 401 =200 /login;
proxy_pass http://backend/;
}
# 这里是我们认证的页面
location /login {
proxy_pass http://backend/login;
# 这个 X-Target 是给认证完以后重定向的
proxy_set_header X-Target $request_uri;
}
# 这是用做 auth_request 请求的路径
location = /auth-proxy {
internal;
# 提供 ldap 认证服务的 auth-proxy backend
# 这个 demo 里是 nginx-ldap-auth-daemon.py.
proxy_pass http://127.0.0.1:8888;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_cache auth_cache;
proxy_cache_valid 200 10m;
# cookie 会加在这里
proxy_cache_key "$http_authorization$cookie_nginxauth";
# ldap 的地址
proxy_set_header X-Ldap-URL "ldap://ldap.example.org";
# 是否开启 starttls
# 注意 starttls 不能和 tls,也就是 ldaps 同时开启
#proxy_set_header X-Ldap-Starttls "true";
# ldap 的 BaseDN
proxy_set_header X-Ldap-BaseDN "dc=example,dc=org";
# ldap 的 binddn,也就是有查询权限的账号
proxy_set_header X-Ldap-BindDN "cn=manager,dc=example,dc=org";
# binddn 的密码
proxy_set_header X-Ldap-BindPass "password";
# cookie 的名字和值
proxy_set_header X-CookieName "nginxauth";
proxy_set_header Cookie nginxauth=$cookie_nginxauth;
# ldap 的 searchFilter,就是拿哪个字段作为认证的用户名
proxy_set_header X-Ldap-Template "(uid=%(username)s)";
}
}
}
а затем выполнить./nginx-ldap-auth-daemon.py
и./backend-sample-app.py
Вот и все.
Посетите порт Nginx 8081, вы можете увидеть, что он может перенаправить наbackend
Идите на сертификацию.
бревно
# ./nginx-ldap-auth-daemon.py
Start listening on localhost:8888...
localhost.localdomain - - [06/Jun/2018 09:18:31] using username/password from authorization header
localhost.localdomain - - [06/Jun/2018 09:18:31] "GET /auth-proxy HTTP/1.0" 401 -
localhost.localdomain - - [06/Jun/2018 09:18:35] using username/password from authorization header
localhost.localdomain - - [06/Jun/2018 09:18:35] "GET /auth-proxy HTTP/1.0" 401 -
localhost.localdomain - - [06/Jun/2018 09:18:43] using username/password from cookie nginxauth
localhost.localdomain - 20150073 [06/Jun/2018 09:18:43] searching on server "ldap://202.120.83.219" with base dn "dc=ecnu,dc=edu,dc=cn" with filter "(uid=20150073)"
localhost.localdomain - 20150073 [06/Jun/2018 09:18:43] Auth OK for user "20150073"
localhost.localdomain - 20150073 [06/Jun/2018 09:18:43] "GET /auth-proxy HTTP/1.0" 200 -
# ./backend-sample-app.py
localhost.localdomain - - [06/Jun/2018 09:18:31] "GET /login HTTP/1.0" 200 -
localhost.localdomain - - [06/Jun/2018 09:18:35] "GET /login HTTP/1.0" 200 -
localhost.localdomain - - [06/Jun/2018 09:18:43] "POST /login HTTP/1.0" 302 -
localhost.localdomain - - [06/Jun/2018 09:18:43] "GET / HTTP/1.0" 200 -
localhost.localdomain - - [06/Jun/2018 09:18:43] "GET /favicon.ico HTTP/1.0" 200 -
анализ кода
весьdemo
Кромеpython-ldap
Других зависимостей нет. этоhttp
Сервис используетHTTPServer
модуль.
Первый взглядbackend-sample-app.py
, это нашdemo
внутреннийbackend
.
Во-первых, это маршрутизация:
def do_GET(self):
url = urlparse.urlparse(self.path)
if url.path.startswith("/login"):
return self.auth_form()
self.send_response(200)
self.end_headers()
self.wfile.write('Hello, world! Requested URL: ' + self.path + '\n')
Как видите, он поддерживает два маршрута. просить/login
Просто пропустите страницу аутентификации, иначе выведитеHello, world!
Тогда посмотрите на эту форму:
# send login form html
def auth_form(self, target = None):
# try to get target location from header
if target == None:
target = self.headers.get('X-Target')
# form cannot be generated if target is unknown
if target == None:
self.log_error('target url is not passed')
self.send_response(500)
return
html="""
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv=Content-Type content="text/html;charset=UTF-8">
<title>Auth form example</title>
</head>
<body>
<form action="/login" method="post">
<table>
<tr>
<td>Username: <input type="text" name="username"/></td>
<tr>
<td>Password: <input type="text" name="password"/></td>
<tr>
<td><input type="submit" value="Login"></td>
</table>
<input type="hidden" name="target" value="TARGET">
</form>
</body>
</html>"""
self.send_response(200)
self.end_headers()
self.wfile.write(html.replace('TARGET', target))
Сама форма аутентификации очень проста, вы можете видеть, что она помещает заголовок http вX-Target
Выньте его и повторно отправьте из формы скрытым способом. этоX-Target
Поле будет использоваться для перенаправления обратно на исходную страницу запроса после аутентификации.
Посмотрите, что делать после получения отправленных данных:
user = form.getvalue('username')
passwd = form.getvalue('password')
target = form.getvalue('target')
if user != None and passwd != None and target != None:
# form is filled, set the cookie and redirect to target
# so that auth daemon will be able to use information from cookie
self.send_response(302)
# WARNING WARNING WARNING
#
# base64 is just an example method that allows to pack data into
# a cookie. You definitely want to perform some encryption here
# and share a key with auth daemon that extracts this information
#
# WARNING WARNING WARNING
enc = base64.b64encode(user + ':' + passwd)
self.send_header('Set-Cookie', 'nginxauth=' + enc + '; httponly')
self.send_header('Location', target)
self.end_headers()
return
Напишите здесь cookie, введите имя пользователя и пароль с помощью;
После подключения выполните base64 для записи cookie. (Обратите внимание, что это очень небезопасно! Не пишите это в рабочей среде). затем написатьLocation
написать вtarget
значение для реализации перенаправления и возврата.
Это грубо, в конце концов, это всего лишь демонстрация.
тогда мы видимnginx-ldap-auth-daemon.py
. Это отвечает за nginxauth_request
ответ на.
Нет файла cookie или неверный файл cookie, возврат 401:
auth_header = self.headers.get('Authorization')
auth_cookie = self.get_cookie(ctx['cookiename'])
if auth_cookie != None and auth_cookie != '':
auth_header = "Basic " + auth_cookie
self.log_message("using username/password from cookie %s" %
ctx['cookiename'])
else:
self.log_message("using username/password from authorization header")
if auth_header is None or not auth_header.lower().startswith('basic '):
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm="' + ctx['realm'] + '"')
self.send_header('Cache-Control', 'no-cache')
self.end_headers()
Файл cookie правильный, base64 декодирует имя пользователя и пароль в:
try:
auth_decoded = base64.b64decode(auth_header[6:])
user, passwd = auth_decoded.split(':', 1)
except:
self.auth_failed(ctx)
return True
ctx['user'] = user
ctx['pass'] = passwd
Затем возьмите имя пользователя и пароль, а также информацию о конфигурации ldap в заголовке http, чтобы выполнить аутентификацию ldap. Эта часть кода довольно длинная, поэтому возьмем короткий фрагмент:
ldap_obj = ldap.initialize(ctx['url']);
# Python-ldap module documentation advises to always
# explicitely set the LDAP version to use after running
# initialize() and recommends using LDAPv3. (LDAPv2 is
# deprecated since 2003 as per RFC3494)
#
# Also, the STARTTLS extension requires the
# use of LDAPv3 (RFC2830).
ldap_obj.protocol_version=ldap.VERSION3
# Establish a STARTTLS connection if required by the
# headers.
if ctx['starttls'] == 'true':
ldap_obj.start_tls_s()
# See http://www.python-ldap.org/faq.shtml
# uncomment, if required
# ldap_obj.set_option(ldap.OPT_REFERRALS, 0)
ctx['action'] = 'binding as search user'
ldap_obj.bind_s(ctx['binddn'], ctx['bindpasswd'], ldap.AUTH_SIMPLE)
ctx['action'] = 'preparing search filter'
searchfilter = ctx['template'] % { 'username': ctx['user'] }
self.log_message(('searching on server "%s" with base dn ' + \
'"%s" with filter "%s"') %
(ctx['url'], ctx['basedn'], searchfilter))
ctx['action'] = 'running search query'
results = ldap_obj.search_s(ctx['basedn'], ldap.SCOPE_SUBTREE,
searchfilter, ['objectclass'], 1)
ctx['action'] = 'verifying search query results'
if len(results) < 1:
self.auth_failed(ctx, 'no objects found')
return
ctx['action'] = 'binding as an existing user'
ldap_dn = results[0][0]
ctx['action'] += ' "%s"' % ldap_dn
ldap_obj.bind_s(ldap_dn, ctx['pass'], ldap.AUTH_SIMPLE)
self.log_message('Auth OK for user "%s"' % (ctx['user']))
# Successfully authenticated user
self.send_response(200)
В основном это стандартный процесс аутентификации ldap.
- брать
binddn
иbindpasswd
Сначала выполните привязку, чтобы получить разрешение на запрос. - взять имя пользователя и
searchFilter
запросить и получитьdn
. - возьми это
dn
Затем выполните привязку пароля пользователя, чтобы выполнить проверку аутентификации ldap.
Производственная среда
Код этой демонстрации, безусловно, не может быть непосредственно применен в производственной среде.HTTPServer
Подходит ли это для производственной среды или нет, я боюсь, что запись имени пользователя и пароля непосредственно в файл cookie недопустима. Даже совместное использование ключа для шифрования, такого как AES, как упоминалось в комментариях, не очень удобно.
На самом деле, этот вид запроса мы обычно делаем через сеанс. После аутентификации пользователя сеанс записывается, и сеанс записывается в файл cookie. В следующий раз, когда пользователь подойдет, чтобы проверить таблицу сеансов, если срок действия сеанса не истек, они могут быть освобождены напрямую.
И нам также нужно наложить некоторые специальные политики, например, некоторые IP-адреса выпускаются напрямую без аутентификации, и выбрать, следует ли включать аутентификацию в соответствии с периодом времени. Все это требует сеансов для поддержки.
Давайте сохраним эту часть для следующего раза.
использованная литература
nginx-ldap-auth
nginx-plus-authenticate-users