Я давно уже полюбил связку 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.
Вот такое решение. Надеюсь, оно кому-то пригодится, или, как минимум, натолкнет на полезные мысли и решения.