Аутентифицируем запросы в микросервисном приложении с помощью nginx и JWT

в 15:36, , рубрики: lua-nginx-module, Microservices, nginx, ruby on rails, Веб-разработка

Стараясь оставаться в тренде и следуя веяниям моды веб разработки, последнее веб приложение я решил реализовать как набор микросервисов на ruby плюс “толстый” клиент на ember. Одна из первых проблем, вставших перед мной была связана с аутентификацией запросов. Если в классическом, монолитном, приложении все просто, используем куки, сессии, подключаем какой-нибудь devise, то тут все как в первый раз.

Архитектура

За базу я выбрал JWT — Json Web Token. Это открытый стандарт RFC 7519 для представления заявок (claims) между двумя участниками. Он представляет из себя структуру вида: Header.Payload.Signature, где заголовок и payload это запакованые в base64 json хэши. Здесь стоит обратить внимание на payload. Он может содержать в себе все что угодно, в принципе это может быть и просто client_id и какая-то другая информация о пользователе, но это не очень хорошая идея, лучше передавать там только ключ идентификатор, а сами данные хранить где-то в другом месте. В качестве хранилища данных можно использовать что угодно, но мне показалось, что redis будет оптимальным, тем более что он пригодится и для других задач. Еще один важный момент — каким ключем мы будем подписывать наш токен. Самый простой вариант использовать один shared key, но это явно не самый безопасный вариант. Коль скоро мы храним данные сессии в redis, ничто не мешает нам генерировать уникальный ключ для каждого токена и хранить его там же.

Понятно, что генерировать токены будет сервис отвечающий за авторизацию, но кто и как будет их проверять? В принципе можно проверку затолкать в каждый микросервис, но это противоречит идеи их максимального разделения. Каждый сервис должен будет содержать логику обработки и проверки токенов да еще и иметь доступ к redis. Нет, наш цель получить архитектуру в которой все запросы приходящие в конечные сервисы уже авторизованы и несут в себе данные о пользователе (например в каком-нибудь специальном заголовке).

Проверка JWT токенов в NGinx

Тут мы и подходим к основной части этой статьи. Нам нужен какой то промежуточный элемент, через который бы проходили все запросы а он их аутентифицировал, заполнял клиентскими данными и посылал дальше. В идеале сервис должен быть легковесным и легко масштабироваться. Очевидным решением будет NGinx reverse proxy, благо мы можем добавить к нему логику аутентификации с помощью lua скриптов. Если быть точным, то мы будем использовать OpenResty — дистрибутив nginx с кучей “плюшек” из коробки. Для пущей красоты реализуем все это в виде Docker контейнера.

Начинать полностью с нуля пришлось. Есть прекрасный проект lua-resty-jwt уже реализующий проверку подписи JWT. Там даже есть пример работы с redis кешем для хранения подписи, осталось только его допилить чтобы:

  1. вытягивать токен из Authorization заголовка
  2. в случае успешной проверки доставать данные сессии и посылать их в X-Data заголовке
  3. немного причесать ошибки, чтобы отдавался валидный JSON

Результат работы можно найти тут: resty-lua-jwt

В nginx.conf нужно прописать в http секцию ссылку на lua пакет:

http {
   ...
   lua_package_path "/lua-resty-jwt/lib/?.lua;;";
   lua_shared_dict jwt_key_dict 10m;
   ...
}

Теперь для того чтобы аутентифицироваться запрос осталось в секцию location довавить:

location ~ ^/api/(.*)$ {
    set $redhost "redis";
    set $redport 6379;
    access_by_lua_file /lua-resty-jwt/jwt.lua;
    proxy_pass http://upstream/api/$1;
}

Запускаем все это дело:

docker run --name redis redis

docker run --link redis -v nginx.conf:/usr/nginx/conf/nginx.conf svyatogor/resty-lua-jwt

И готово… ну почти. Надо еще положить в redis сессию и отдать клиенту его токен. jwt.lua плагин ожидает, что токен в своей Payload секции будет содержать хэш виа {kid: SESSION_ID}. В redis этому SESSION_ID должен соответствовать хэш как минимум с одним ключем secret, в котором находится общий ключ для проверки подписи. Еще там может быть ключ data, если он найдет то его содержимое уйдет в upstream сервис в заголовке X-Data. В этот ключ мы сложим сериализованый объект пользователя, ну или, как минимум, его ID, чтобы апстрим сервис понимал от кого же пришел запрос.

Логин и генерация токенов

Для генерации JWT есть великое множество библиотек, полное описание тут: jwt.io В моем случае я выбрал jwt гем. Вот как выглядит action SessionController#create

def new
    user = User.find_by_email params[:email]
    if user && user.authenticate(params[:password])
        if user.kid and REDIS.exists(user.kid) > 0
            REDIS.del user.kid
        end

        key = SecureRandom.base64(24)
        secret = SecureRandom.base64(24)
        REDIS.hset key, 'secret', secret
        REDIS.hset key, 'data', {user_id: user.id}.to_json

        payload = {"kid" => key}
        token = JWT.encode payload, secret, 'HS256'
        render json: {token: token}
    else
        render json: {error: "Invalid username or password"}, status: 401
    end
end

Теперь в нашем UI (ember, angular или же мобильное приложение) нужно получить у authorization сервиса токен и передавать его во всех запросах в заголовке Authorization. Как именно вы это будете делать зависит от вашего конкретного случая, так что я приведу лишь пример с cUrl.

$ curl -X POST http://default/auth/login -d 'email=user@mail.com' -d 'password=user'
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8YgxyFw0ccgrFza1Uxr8Q_U9z3dlWdzpSYo"}%

$ curl http://default/clients/v1/clients -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJraWQiOiI2cDFtdFBrVnhUdTlVNngxTk5yaTNPSDVLcnBGVzZRUCJ9.9Qawf8PE8Ygxy
Fw0ccgrFza1Uxr8Q_U9z3dlWdzpSYo'
{"clients":[]}

Послесловие

Логично будет поинтересоваться, есть ли готовые решения? Я нашел только Kong от Mashape. Для когото это будет неплохим варинатом, т.к. кроме разных видов авторизации он умеет работать с ACL, управлять нагрузкой применять ACL и много чего еще. В моем случае это была бы стрельба из пушки по воробьям. Кроме того он зависит от БД Casandra, которая, мягко скажем, тажеловата да и довольно чужеродна этому проекту.

P.P.S. Незаметно "добрые люди" слили карму. Так что плюсик будет очень кстати и будет хорошей мотивацией к написанию новых статей на тему микросервисов в веб разработке.

Автор: svyatogor

Источник



https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js