Кэширование аутентифицированных запросов для всех пользователей

9

Я работаю над веб-приложением, которое должно иметь дело с очень большими импульсами одновременных пользователей, которым требуется авторизация, для запроса идентичного контента. В своем нынешнем состоянии он полностью наносит ущерб даже 32-ядерному экземпляру AWS.

(Обратите внимание, что мы используем Nginx в качестве обратного прокси)

Ответ не может быть просто кэширован, так как в худшем случае мы должны проверить, аутентифицирован ли пользователь путем декодирования его JWT. Это требует от нас запуска Laravel 4, который, по мнению большинства, идет медленно , даже с включенными PHP-FPM и OpCache. Это в основном из-за здоровенной фазы начальной загрузки.

Кто-то может задать вопрос: «Почему вы в первую очередь использовали PHP и Laravel, если знали, что это будет проблемой?» - но уже слишком поздно возвращаться к этому решению!

Возможное решение

Одно решение, которое было предложено, состоит в том, чтобы извлечь модуль Auth из Laravel в легкий внешний модуль (написанный на чем-то быстром, как C), чья ответственность заключается в том, чтобы декодировать JWT и решить, аутентифицирован ли пользователь.

Поток запроса будет:

  1. Проверьте, не попал ли в кеш (если не перешел на PHP как обычно)
  2. Расшифровать токен
  3. Проверьте, действительно ли это
  4. Если допустимо , подавать из кеша
  5. Если он недействителен , сообщите Nginx, а затем Nginx передаст запрос в PHP для обработки в обычном режиме.

Это позволит нам не использовать PHP после того, как мы передадим этот запрос одному пользователю, и вместо этого обратиться к легковесному модулю, чтобы возиться с декодированием JWT и любыми другими предупреждениями, которые идут с этим типом аутентификации.

Я даже думал написать этот код напрямую как модуль расширения Nginx HTTP.

Обеспокоенность

Меня беспокоит то, что я никогда не видел, чтобы это было сделано раньше, и задавался вопросом, есть ли лучший способ.

Кроме того, когда вы добавляете какой-либо пользовательский контент на страницу, этот метод полностью убивает.

Есть ли другое простое решение, доступное непосредственно в Nginx? Или нам нужно использовать что-то более специализированное, например, Varnish?

Мои вопросы:

Имеет ли смысл приведенное выше решение?

Как это обычно достигается?

Есть ли лучший способ добиться такого же или лучшего прироста производительности?

iamyojimbo
источник
Я борюсь с аналогичной проблемой. Пара идей a) Nginx auth_request может быть в состоянии передать ваш микросервис аутентификации, что избавит от необходимости разработки модуля Nginx. б) В качестве альтернативы, ваш микросервис может перенаправить аутентифицированных пользователей на временный URL-адрес, который является открытым, кэшируемым и неуязвимым, но может быть проверен серверной частью PHP на срок действия в течение ограниченного периода (периода кэширования). Это жертвует некоторой безопасностью: если временный URL-адрес просочился ненадежному пользователю, он может получить доступ к контенту в течение этого ограниченного периода, во многом как токен носителя OAuth.
Джеймс
Вы придумали решение этого? Я сталкиваюсь с тем же самым
тембродер
Оказывается, имея большой кластер оптимизированных внутренних узлов, мы смогли справиться с нагрузкой, но я уверен, что этот подход является долгосрочным решением с большими затратами. Если вам известны некоторые ответы, которые вы могли бы обслуживать заранее, если вы согреваете кэш перед притоком запросов, экономия ресурсов бэкэнда и повышение надежности будут очень высокими.
iamyojimbo

Ответы:

9

Я пытался решить аналогичную проблему. Мои пользователи должны проходить аутентификацию для каждого запроса, который они делают. Я сосредоточился на том, чтобы аутентифицировать пользователей хотя бы один раз с помощью бэкэнд-приложения (проверка токена JWT), но после этого я решил, что мне больше не нужен бэкэнд.

Я решил не требовать плагин Nginx, который не включен по умолчанию. В противном случае вы можете проверить скрипты nginx-jwt или Lua, и это, вероятно, будет отличным решением.

Адресация аутентификации

До сих пор я сделал следующее:

  • Делегировал аутентификацию Nginx, используя auth_request. Это вызывает internalместоположение, которое передает запрос к моей конечной точке проверки токена бэкэнда. Уже одно это не решает проблему обработки большого количества проверок.

  • Результат проверки токена кэшируется с помощью proxy_cache_key "$cookie_token";директивы. После успешной проверки токена серверная часть добавляет Cache-Controlдирективу, которая указывает Nginx кэшировать токен только на срок до 5 минут. На этом этапе любой токен аутентификации, проверенный один раз, находится в кэше, последующие запросы от того же пользователя / токена больше не затрагивают бэкэнд аутентификации!

  • Чтобы защитить мое внутреннее приложение от возможного переполнения недействительными токенами, я также кэшировал отклоненные проверки, когда моя конечная точка внутреннего сервера возвращает 401. Эти кэшируются только на короткое время, чтобы избежать потенциального заполнения кеша Nginx такими запросами.

Я добавил несколько дополнительных улучшений, таких как конечная точка выхода из системы, которая делает недействительным токен, возвращая 401 (который также кэшируется Nginx), так что если пользователь нажимает кнопку выхода из системы, токен больше не может использоваться, даже если срок его действия не истек.

Кроме того, мой кеш Nginx содержит для каждого токена связанный пользователь в виде объекта JSON, что избавляет меня от необходимости извлекать его из БД, если мне нужна эта информация; а также спасает меня от расшифровки токена.

О времени жизни токенов и обновления токенов

Через 5 минут токен истечет в кеше, поэтому бэкэнд снова будет запрошен. Это необходимо для того, чтобы вы могли сделать недействительным токен, потому что пользователь вышел из системы, потому что он был взломан и так далее. Такая периодическая повторная проверка с надлежащей реализацией в бэкэнде избавляет меня от необходимости использовать токены обновления.

Традиционно токены обновления будут использоваться для запроса нового токена доступа; они будут храниться в вашем бэкэнде, и вы проверите, что запрос на токен доступа сделан с токеном обновления, который совпадает с тем, который у вас есть в базе данных для этого конкретного пользователя. Если пользователь выходит из системы или токены скомпрометированы, вы должны удалить / аннулировать токен обновления в вашей БД, чтобы следующий запрос на новый токен с использованием недействительного токена обновления завершился неудачно.

Короче говоря, токены обновления обычно имеют длительный срок действия и всегда сверяются с бэкэндом. Они используются для генерации токенов доступа, которые имеют очень короткий срок действия (несколько минут). Эти токены доступа обычно достигают вашего бэкэнда, но вы проверяете только их подпись и дату окончания срока действия.

Здесь, в моей настройке, мы используем токены с более длительным сроком действия (могут быть часы или день), которые имеют ту же роль и функции, что и токен доступа и токен обновления. Поскольку их валидация и аннулирование кэшируются Nginx, они полностью проверяются бэкэндом раз в 5 минут. Таким образом, мы сохраняем преимущество использования токенов обновления (чтобы иметь возможность быстро аннулировать токен) без дополнительной сложности. И простая проверка никогда не достигает вашего бэкэнда, который по крайней мере на 1 порядок медленнее, чем кэш Nginx, даже если он используется только для проверки подписи и даты истечения срока действия.

С помощью этой настройки я мог отключить аутентификацию в своем бэкэнде, так как все входящие запросы достигают auth_requestдирективы Nginx, прежде чем ее трогать.

Это не решает проблему полностью, если вам нужно выполнить какую-либо авторизацию для каждого ресурса, но, по крайней мере, вы сохранили основную часть авторизации. И вы даже можете избежать расшифровки токена или выполнить поиск в БД для доступа к данным токена, поскольку кэшированный ответ аутентификации Nginx может содержать данные и передавать их обратно бэкэнду.

Сейчас моя самая большая проблема в том, что я могу нарушить что-то очевидное, связанное с безопасностью, не осознавая этого. При этом любой полученный токен по-прежнему проверяется, по крайней мере, один раз, прежде чем будет кэширован Nginx. Любой закаленный токен будет отличаться, поэтому не попадет в кеш, так как ключ кеша также будет другим.

Также, возможно, стоит упомянуть, что аутентификация в реальном мире будет бороться с кражей токенов, генерируя (и проверяя) дополнительный одноразовый номер или что-то в этом роде.

Вот упрощенный фрагмент моей конфигурации Nginx для моего приложения:

# Cache for internal auth checks
proxy_cache_path /usr/local/var/nginx/cache/auth levels=1:2 keys_zone=auth_cache:10m max_size=128m inactive=10m use_temp_path=off;
# Cache for content
proxy_cache_path /usr/local/var/nginx/cache/resx levels=1:2 keys_zone=content_cache:16m max_size=128m inactive=5m use_temp_path=off;
server {
    listen 443 ssl http2;
    server_name ........;

    include /usr/local/etc/nginx/include-auth-internal.conf;

    location /api/v1 {
        # Auth magic happens here
        auth_request         /auth;
        auth_request_set     $user $upstream_http_X_User_Id;
        auth_request_set     $customer $upstream_http_X_Customer_Id;
        auth_request_set     $permissions $upstream_http_X_Permissions;

        # The backend app, once Nginx has performed internal auth.
        proxy_pass           http://127.0.0.1:5000;
        proxy_set_header     X-User-Id $user;
        proxy_set_header     X-Customer-Id $customer;
        proxy_set_header     X-Permissions $permissions;

        # Cache content
        proxy_cache          content_cache;
        proxy_cache_key      "$request_method-$request_uri";
    }
    location /api/v1/Logout {
        auth_request         /auth/logout;
    }

}

Теперь вот экстракт конфигурации для внутренней /authконечной точки, включенный выше как /usr/local/etc/nginx/include-auth-internal.conf:

# Called before every request to backend
location = /auth {
    internal;
    proxy_cache             auth_cache;
    proxy_cache_methods     GET HEAD POST;
    proxy_cache_key         "$cookie_token";
    # Valid tokens cache duration is set by backend returning a properly set Cache-Control header
    # Invalid tokens are shortly cached to protect backend but not flood Nginx cache
    proxy_cache_valid       401 30s;
    # Valid tokens are cached for 5 minutes so we can get the backend to re-validate them from time to time
    proxy_cache_valid       200 5m;
    proxy_pass              http://127.0.0.1:1234/auth/_Internal;
    proxy_set_header        Host ........;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length "";
    proxy_set_header        Accept application/json;
}

# To invalidate a not expired token, use a specific backend endpoint.
# Then we cache the token invalid/401 response itself.
location = /auth/logout {
    internal;
    proxy_cache             auth_cache;
    proxy_cache_key         "$cookie_token";
    # Proper caching duration (> token expire date) set by backend, which will override below default duration
    proxy_cache_valid       401 30m;
    # A Logout requests forces a cache refresh in order to store a 401 where there was previously a valid authorization
    proxy_cache_bypass      1;

    # This backend endpoint always returns 401, with a cache header set to the expire date of the token
    proxy_pass              http://127.0.0.1:1234/auth/_Internal/Logout;
    proxy_set_header        Host ........;
    proxy_pass_request_body off;
}

,

Адресация содержания контента

Теперь аутентификация отделена от данных. Поскольку вы сказали, что он идентичен для каждого пользователя, сам контент также может кэшироваться Nginx (в моем примере, в content_cacheзоне).

Масштабируемость

Этот сценарий прекрасно работает из коробки, если у вас есть один сервер Nginx. В реальном сценарии вы, вероятно, обладаете высокой доступностью, то есть несколькими экземплярами Nginx, потенциально также размещая ваше (Laravel) внутреннее приложение. В этом случае любой запрос, сделанный вашими пользователями, может быть отправлен на любой из ваших серверов Nginx, и пока все они локально не кэшируют токен, они будут продолжать обращаться к вашему бэкэнду, чтобы проверить его. Для небольшого количества серверов использование этого решения все равно принесет большие выгоды.

Однако важно отметить, что с несколькими серверами Nginx (и, следовательно, кешами) вы теряете возможность выхода из системы на стороне сервера, потому что вы не можете очистить (путем принудительного обновления) кеш токенов на всех из них, например, /auth/logoutделает в моем примере. У вас останется только 5-минутная длительность кэша токена, которая вскоре заставит ваш бэкэнд запросить и сообщит Nginx, что запрос отклонен. Частичным обходным решением является удаление заголовка токена или файла cookie на клиенте при выходе из системы.

Любой комментарий будет очень приветствоваться и ценится!

mbarthelemy
источник
Вы должны получить намного больше голосов! Очень полезно, спасибо!
Гершон Папи
«Я добавил несколько дополнительных улучшений, таких как конечная точка выхода из системы, которая делает недействительным токен, возвращая 401 (который также кэшируется Nginx), так что если пользователь нажимает кнопку выхода из системы, токен больше не может использоваться, даже если срок его действия не истек. " - Это умно! , но вы на самом деле занесете в черный список токен и в своем бэкэнде, чтобы в случае сбоя кэша или чего-то еще пользователь не мог войти в систему с этим конкретным токеном?
gaurav5430
«Однако важно отметить, что с несколькими серверами Nginx (и, следовательно, кэшами) вы теряете возможность выхода из системы на стороне сервера, потому что вы не можете очистить (путем принудительного обновления) кэш токенов на всех них, как / auth / logout делает в моем примере. " можешь уточнить?
gaurav5430