Меняем KrakenD на OpenResty
В предыдущем посте я писал о выборе API шлюза и как KrakenD позволил нам склеивать ответы от различных API. В итоге мы отказались от KrakenD в пользу OpenResty и я расскажу почему.
KrakenD – это современный API-шлюз, написанный на Go, с кучей фич. Работает классно, быстро. Имеет интеграцию с инструментами Open Telemetry и много чем еще, но… в Community Edition не включена работа с шаблонами в URL сервисов. И это печально. Нужно повторять описания каждой точки раз за разом (а это минимум 10 строк в нашем случае). Что-то вроде:
{
"endpoint": "/api/ideas",
"method": "GET",
"backend": [
{
"url_pattern": "/api/ideas",
"host": [ "{{ .services.ideas }}" ]
}
],
{{ include "auth.json" }}
},
На данный момент у нас около десятка сервисов по несколько точек в каждом и поддержка уже сулит сущий кошмар. Мы стояли перед выбором продолжать увеличивать масштаб трагедии или пересмотреть подход пока все не зашло слишком далеко.
По сути, что нам нужно от решения:
-
Поддерживать аутентификацию JWT. Мы получаем токены в Authorization заголовке и Cookies от клиента в каждом запросе, хотим проверять их подпись и перепаковывать некоторые поля в заголовки запросов для сервисов ниже по течению.
-
Иметь гибкие инструменты маршрутизации. У нас десятки точек в различных сервисах.
-
Быть бесплатным и с открытым кодом. Пока это хобби-проект нам совсем не хочется платить деньги за лицензии, планы и прочие коммерческие блага.
Как оказалось, выбор не сильно велик. Я посмотрел несколько популярных решений и не нашел ни одного, которое бы покрывало все три требования, кроме…
Вы уже догадались. OpenResty. По правде сказать, я восхищен решением и одновременно удивлен тем, что мысль посмотреть в сторону NGINX + Lua так долго шла в мою голову. Наверное, я – жираф. Как они сами пишут, OpenResty – это “a dynamic web platform based on NGINX and LuaJIT”. На сколько мне удалось понять из статей и разговоров с DevOps спецами, это очень популярное решение, очень производительное и гибкое. Самое главное это то, что оно предоставляет поддержку Lua везде где можно – в конфигах, в отдельных модулях. LuaJIT по скорости, конечно, уступает коду на C, но за счет JIT компиляции модули на Lua находятся где-то очень и очень близко.
В качестве концепта я потратил пару часов на написание своей верификации и перепаковки JWT с нуля. Подсмотрел пару приемов, взял библиотечку для работы с OpenIDC и вот что получилось:
# Authentication
#
# All endpoints are authenticated by default.
# JWT token is taken from Authentication header Bearer, signature is verified and subject (sub).
# If Authentication header is missing, a cookie with the same name is attempted.
# is sent further as an X-Employee-ID header.
#
# To skip authentication, you can do the following:
#
# location /api/public-endpoint {
# access_by_lua_block { }
# }
access_by_lua_block {
-- Check that we have authorization header
local auth_header = ngx.req.get_headers()["Authorization"]
if not auth_header then
-- Use authorization header from cookie "AccessToken"
local cookie_value = ngx.var["cookie_accesstoken"]
if not cookie_value then
ngx.exit(ngx.HTTP_UNAUTHORIZED)
else
ngx.req.set_header("Authorization", "Bearer " .. cookie_value)
end
end
-- Verify auth token and raise an error if invalid
local openidc = require("resty.openidc")
local opts = { discovery = { jwks_uri = "http://auth:5000/api/.well-known/jwks" } }
local json, err = openidc.bearer_jwt_verify(opts)
if err then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(err)
return
end
ngx.req.set_header("x-employee-id", json.sub)
}
Этот небольшой кусочек берет токен из заголовка Authorization
, а если там его нет, то из Cookie AccessToken
. Затем забирает JWKS свзяку ключей из определенного URL (и кэширует их конечно) и проверяет подпись JWT ключом из связки. Если все хорошо, то в заголовок X-Employee-ID
вставляется содержимое JWT sub
и запрос отправляется дальше. А если токен не пришел, был битым или неправильно подписанным, выдается 403 UNAUTHORIZED
.
Вот и все. Работает надежно.