Рубрики
Web-Разработка

Прозрачная авторизация сервисов в гетерогенной среде на базе Nginx/LUA

Я давно уже полюбил связку 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 lua architecture

Вот примерный конфиг 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.

Вот такое решение. Надеюсь, оно кому-то пригодится, или, как минимум, натолкнет на полезные мысли и решения.