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

от автора

Стараясь оставаться в тренде и следуя веяниям моды веб разработки, последнее веб приложение я решил реализовать как набор микросервисов на 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. Незаметно "добрые люди" слили карму. Так что плюсик будет очень кстати и будет хорошей мотивацией к написанию новых статей на тему микросервисов в веб разработке.

ссылка на оригинал статьи https://habrahabr.ru/post/277677/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *