Я давно уже полюбил связку Nginx и LUA. И сегодня хочу рассказать про один вариант использования этих инструментов.
Суть задачи/проблемы:
Представьте, что в вашей информационной системе есть набор различных сервисов, с которыми могут взаимодействовать клиентские приложения. Конечно же, доступ к системе и к сервисам, в частности, разрешается после авторизации.
Все сервисы общаются с клиентами по разным протоколам (HTTP REST API, SOAP, XML-RPC, WebSocket, WAMP, GET/POST запросы и так далее) и даже написаны на разных языках.
Вопрос: как минимальными усилиями сделать, чтобы доступ к любым сервисам был возможен только авторизованным пользователям?
Рассуждения и возможные способы решения:
Первое, что приходит в голову — это написать модуль авторизации для каждого сервиса и пусть сервис сам проверяет все входящие запросы. Минусы этого подхода достаточно очевидны:
- Вам придется, как минимум, написать ровно столько реализаций модуля авторизации, сколько у вас различных языков, на которых написаны сервисы.
- И в дальнейшем вам придется поддерживать все эти модули.
- Так же, для начала придется четко описать и указать как должна быть устроена авторизация, иначе разработчики на разных языках напишут свои собственные велосипеды в соответствии со своим личным чувством прекрасного.
Другой вариант — это написать один сервис, который будет отвечать за авторизацию и проксировать все запросы уже на соответствующие сервисы. Отличный вариант! Но писать с нуля такой сервис, попутно решая задачи балансировки, масштабируемости, отказоустойчивости и прочие — совсем не хочется. У нас же есть Nginx и LUA!
Основная идея такого решения в следующем:
Поскольку REST/WebSocket/WAMP/SOAP/XML-RPC и прочее — это все подходы/протоколы, основанные на HTTP, Nginx будет являться входной точной всех запросов, далее, на этапе обработки запроса выполняется lua скрипт, который проверяет валидность сессии, например в базе данных, и если все ок, nginx проксирует запрос на соответствующий сервис. Так же мы запрещаем доступ к сервисам извне. В случае, если все сервисы живут в рамках одного сервера, переводим их на прослушивание только на localhost’е. Так же отмечу, что LUA модуль работает поверх NGINX cosockets & subrequests с неблокирующим I/O.
Вот примерный конфиг nginx:
server {
listen 80;
server_name http-control;
error_log /var/log/nginx/http-control.error.log;
access_log /var/log/nginx/http-control.access.log main;
client_max_body_size 64m;
charset utf-8;
root /Users/kostik/Projects/http-control;
access_by_lua_file $document_root/src/http-control.lua;
# Proxy for Service 1
location /srv1/ {
rewrite /srv1/(.*) /$1 break;
proxy_pass http://127.0.0.1:8001;
}
# Proxy for Service 2
location /srv2/ {
rewrite /srv2/(.*) /$1 break;
proxy_pass http://127.0.0.1:8002;
}
# Proxy for Service 3
location /srv3/ {
rewrite /srv3/(.*) /$1 break;
proxy_pass http://127.0.0.1:8003;
}
}
И вот каким может быть lua-обработчик запросов. В данном примере происходит подключение к локальному MySQL серверу, и выполняется запрос на проверку наличия в БД сессии пользователя с такого IP-адреса и с таким идентификатором сессии. В случае ошибки на любом шаге, возвращается статус «403: Forbidden». Пример упрощен, чтобы показать суть, и не стоит обращать внимание на тему SQL-инъекций и прочих штук.
local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
-- ngx.say("failed to instantiate mysql: ", err)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
db:set_timeout(1000) -- 1 sec
-- connect to a unix domain socket file listened
-- by a mysql server:
local ok, err, errno, sqlstate =
db:connect{
path = "/tmp/mysql.sock",
database = "application_db",
user = "http-control",
password = "http-control" }
if not ok then
-- ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
local userSession = ngx.req.get_headers()["x-user-session"]
if not userSession then
-- ngx.say("Required header not specified")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
local res, err, errno, sqlstate =
db:query("SELECT count(*) AS cnt
FROM UsersActivity
WHERE session_id = '" .. userSession ..
"' AND client_ip_address = '" .. ngx.var.remote_addr .. "'")
if not res then
-- ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
if tonumber(res[1].cnt) == 0 then
-- ngx.say("No active session found")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- put connection into the connection pool of size 50,
-- with 10 seconds max idle timeout
local ok, err = db:set_keepalive(10000, 50)
if not ok then
-- ngx.say("failed to set keepalive: ", err)
return
end
--ngx.exit(ngx.HTTP_OK)
Можно легко адаптировать данный lua-скрипт, чтобы он ходил в memcache, redis, postgres или даже просто сам обращался по сокету или вебсокету на какой-то внутренний сервис. На openresty.org можно посмотреть на большой список уже написанных модулей.
Так же можно (и даже нужно) сделать, чтобы все клиентские приложения общались с фронтом сервисов через https, а запросы на непосредственно сервисы nginx будет проксировать по http, тем самым снижая накладные расходы и избавляя сервисы от необходимости поддержки SSL/TLS.
Вот такое решение. Надеюсь, оно кому-то пригодится, или, как минимум, натолкнет на полезные мысли и решения.