Аутенфицируем запити в микросервисном додатку за допомогою 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. Непомітно "добрі люди" злили карму. Так що плюсик буде дуже до речі і буде гарною мотивацією до написання нових статей на тему микросервисов у веб-розробці.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.