Buforowanie uwierzytelnionych żądań dla wszystkich użytkowników

9

Pracuję nad aplikacją internetową, która musi radzić sobie z bardzo dużymi impulsami równoczesnych użytkowników, którzy muszą być upoważnieni do żądania identycznych treści. W obecnym stanie jest całkowicie paraliżujący nawet dla 32-rdzeniowej instancji AWS.

(Pamiętaj, że używamy Nginx jako odwrotnego proxy)

Odpowiedzi nie można po prostu zapisać w pamięci podręcznej, ponieważ w najgorszym przypadku musimy sprawdzić, czy użytkownik jest uwierzytelniony poprzez dekodowanie JWT. To wymaga od nas uruchomienia Laravela 4, co większość zgodziłaby się, jest powolna , nawet przy włączonym PHP-FPM i OpCache. Wynika to głównie z dużej fazy ładowania.

Można by zadać pytanie „Dlaczego używałeś PHP i Laravela, jeśli wiedziałeś, że to będzie problem?” - ale jest już za późno, aby wrócić do tej decyzji!

Możliwe rozwiązanie

Jednym z zaproponowanych rozwiązań jest wypakowanie modułu Auth z Laravela do lekkiego modułu zewnętrznego (napisanego w czymś szybkim jak C), którego zadaniem jest zdekodowanie JWT i decyzja, czy użytkownik jest uwierzytelniony.

Przepływ żądania byłby następujący:

  1. Sprawdź, czy pamięć podręczna nie trafiła (jeśli nie przechodzi normalnie do PHP)
  2. Dekoduj token
  3. Sprawdź, czy jest poprawny
  4. Jeśli jest poprawny , podawaj z pamięci podręcznej
  5. Jeśli jest niepoprawny , powiedz Nginx, a wtedy Nginx przekaże żądanie do PHP, aby postępować normalnie.

Pozwoli nam to nie trafić do PHP po tym, jak doręczymy tę prośbę jednemu użytkownikowi, i zamiast tego sięgniemy do lekkiego modułu, aby zadzierać z dekodowaniem JWT i wszelkich innych zastrzeżeń, które są dostarczane z tym rodzajem autoryzacji.

Myślałem nawet o napisaniu tego kodu bezpośrednio jako modułu rozszerzenia HTTP Nginx.

Obawy

Obawiam się, że nigdy wcześniej tego nie widziałem i zastanawiałem się, czy jest lepszy sposób.

Ponadto, po dodaniu do strony treści specyficznych dla użytkownika, całkowicie zabija tę metodę.

Czy istnieje inne prostsze rozwiązanie dostępne bezpośrednio w Nginx? Czy też musielibyśmy użyć czegoś bardziej specjalistycznego, takiego jak Lakier?

Moje pytania:

Czy powyższe rozwiązanie ma sens?

Jak do tego zwykle podejść?

Czy istnieje lepszy sposób na osiągnięcie podobnego lub lepszego wzrostu wydajności?

iamyojimbo
źródło
Zmagam się z podobnym problemem. Kilka pomysłów a) Nginx auth_request może być w stanie przekazać mikrousługę uwierzytelnienia, co zmniejsza potrzebę opracowania modułu Nginx. b) Alternatywnie, twoja mikrousługa może przekierowywać uwierzytelnionych użytkowników na tymczasowy adres URL, który jest publiczny, buforowany i niemożliwy do odczytania, ale może być sprawdzony przez backend PHP, aby był ważny przez ograniczony okres (okres buforowania). Poświęca to pewne bezpieczeństwo, jeśli tymczasowy adres URL wycieknie do niezaufanego użytkownika, może on uzyskać dostęp do treści przez ten ograniczony czas, podobnie jak token okaziciela OAuth.
James
Czy znalazłeś rozwiązanie tego problemu? Mam do czynienia z tym samym
timbroder,
Okazuje się, że dysponując dużym klastrem zoptymalizowanych węzłów zaplecza, byliśmy w stanie poradzić sobie z obciążeniem - ale jestem przekonany, że to podejście jest długoterminowym rozwiązaniem zapewniającym duże oszczędności. Jeśli znasz niektóre z odpowiedzi, które możesz podać z wyprzedzeniem, jeśli ogrzejesz pamięć podręczną przed napływem żądań, oszczędność zasobów zaplecza i wzrost niezawodności byłyby bardzo duże.
iamyojimbo,

Odpowiedzi:

9

Próbowałem rozwiązać podobny problem. Moi użytkownicy muszą być uwierzytelniani przy każdym zgłoszonym przez siebie żądaniu. Koncentrowałem się na uwierzytelnieniu użytkowników przynajmniej raz przez aplikację backendu (sprawdzanie poprawności tokena JWT), ale potem zdecydowałem, że nie będę już potrzebował backendu.

Zdecydowałem się uniknąć wymagania jakiejkolwiek wtyczki Nginx, która nie jest domyślnie dołączona. W przeciwnym razie możesz sprawdzić skrypty nginx-jwt lub Lua i prawdopodobnie byłyby to świetne rozwiązania.

Uwierzytelnianie adresowania

Do tej pory zrobiłem następujące:

  • Delegowanie uwierzytelnienia do Nginx przy użyciu auth_request. To wywołuje internallokalizację, która przekazuje żądanie do mojego punktu końcowego sprawdzania poprawności tokena. Samo to w ogóle nie rozwiązuje problemu obsługi dużej liczby walidacji.

  • Wynik sprawdzania poprawności tokena jest buforowany przy użyciu proxy_cache_key "$cookie_token";dyrektywy. Po pomyślnym sprawdzeniu poprawności tokena, backend dodaje Cache-Controldyrektywę, która mówi Nginxowi, aby buforował token tylko do 5 minut. W tym momencie każdy zatwierdzony token uwierzytelnienia znajduje się w pamięci podręcznej, kolejne żądania od tego samego użytkownika / tokena nie dotykają już zaplecza uwierzytelniania!

  • Aby zabezpieczyć moją aplikację backend przed potencjalnym zalaniem przez nieprawidłowe tokeny, buforowałem również odrzucone walidacje, gdy mój punkt końcowy backend zwraca 401. Te są buforowane tylko przez krótki czas, aby uniknąć potencjalnego zapełnienia pamięci podręcznej Nginx takimi żądaniami.

Dodałem kilka dodatkowych ulepszeń, takich jak punkt końcowy wylogowania, który unieważnia token, zwracając 401 (który jest również buforowany przez Nginx), tak że jeśli użytkownik kliknie wylogowanie, token nie może być już używany, nawet jeśli nie wygasł.

Ponadto moja pamięć podręczna Nginx zawiera dla każdego tokena powiązanego użytkownika jako obiekt JSON, co chroni mnie przed pobraniem go z bazy danych, jeśli potrzebuję tych informacji; a także ratuje mnie przed odszyfrowaniem tokena.

Informacje o czasie życia tokenu i odświeżaniu tokenów

Po 5 minutach token wygaśnie w pamięci podręcznej, więc backend zostanie ponownie zapytany. Ma to na celu zapewnienie, że możesz unieważnić token, ponieważ użytkownik wylogowuje się, ponieważ został przejęty i tak dalej. Takie okresowe przedłużanie ważności, z odpowiednią implementacją w backend, pozwala mi uniknąć używania tokenów odświeżania.

Tradycyjnie tokeny odświeżające byłyby używane do żądania nowego tokena dostępu; będą one przechowywane w wewnętrznej bazie danych, a użytkownik zweryfikuje, czy prośba o token dostępu została wysłana za pomocą tokenu odświeżania pasującego do tego, który użytkownik ma w bazie danych. Jeśli użytkownik wyloguje się lub tokeny zostaną naruszone, usuniesz / unieważnisz token odświeżania w swojej bazie danych, tak aby następne żądanie nowego tokena przy użyciu unieważnionego tokenu odświeżania zakończyło się niepowodzeniem.

Krótko mówiąc, tokeny odświeżania zwykle mają długi okres ważności i zawsze są sprawdzane względem wewnętrznej bazy danych. Służą do generowania tokenów dostępu, które mają bardzo krótki okres ważności (kilka minut). Te tokeny dostępu zwykle docierają do twojego zaplecza, ale sprawdzasz tylko ich podpis i datę ważności.

W moim ustawieniu używamy tokenów o dłuższym okresie ważności (może to być godziny lub dzień), które pełnią tę samą rolę i funkcje co token dostępu i token odświeżania. Ponieważ mamy buforowanie ich sprawdzania poprawności i unieważnienia przez Nginx, są one w pełni weryfikowane przez backend tylko raz na 5 minut. Zachowujemy więc korzyść z używania tokenów odświeżania (jesteśmy w stanie szybko unieważnić token) bez dodatkowej złożoności. A prosta weryfikacja nigdy nie dociera do backendu, który jest co najmniej o 1 rząd wielkości wolniejszy niż pamięć podręczna Nginx, nawet jeśli jest używany tylko do sprawdzania podpisu i daty ważności.

Dzięki tej konfiguracji mogę wyłączyć uwierzytelnianie w moim backendie, ponieważ wszystkie przychodzące żądania docierają do auth_requestdyrektywy Nginx przed jej dotknięciem.

Nie rozwiązuje to w pełni problemu, jeśli musisz wykonać dowolną autoryzację dla jednego zasobu, ale przynajmniej zapisałeś podstawową część autoryzacji. I możesz nawet uniknąć odszyfrowania tokena lub przeszukać bazę danych, aby uzyskać dostęp do danych tokenu, ponieważ buforowana odpowiedź autoryzacji Nginx może zawierać dane i przekazywać je z powrotem do backendu.

Teraz moim największym zmartwieniem jest to, że mogę nie wiedzieć o czymś oczywistym związanym z bezpieczeństwem. To powiedziawszy, każdy otrzymany token jest sprawdzany przynajmniej raz, zanim zostanie zbuforowany przez Nginx. Każdy temperowany token byłby inny, więc nie trafiałby do pamięci podręcznej, ponieważ klucz pamięci podręcznej również byłby inny.

Być może warto wspomnieć, że autentyczne uwierzytelnianie w świecie walczyłoby z kradzieżą tokenów poprzez generowanie (i weryfikację) dodatkowej wartości jednorazowej lub czegoś takiego.

Oto uproszczony fragment mojej konfiguracji Nginx dla mojej aplikacji:

# 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;
    }

}

Oto wyciąg z konfiguracji dla wewnętrznego /authpunktu końcowego, uwzględniony powyżej jako /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;
}

.

Adresowanie treści wyświetlanych

Teraz uwierzytelnianie jest oddzielone od danych. Ponieważ powiedziałeś, że jest identyczny dla każdego użytkownika, sama zawartość może być buforowana przez Nginx (w moim przykładzie w content_cachestrefie).

Skalowalność

Ten scenariusz działa świetnie od razu po założeniu, że masz jeden serwer Nginx. W scenariuszu ze świata rzeczywistego zapewne masz wysoką dostępność, co oznacza wiele instancji Nginx, potencjalnie również hostując twoją aplikację (Laravel). W takim przypadku każde żądanie wysłane przez użytkowników może zostać wysłane na dowolny z serwerów Nginx i dopóki wszyscy lokalnie nie zbuforują tokena, będą docierać do backendu, aby go zweryfikować. W przypadku niewielkiej liczby serwerów korzystanie z tego rozwiązania nadal przyniosłoby duże korzyści.

Należy jednak pamiętać, że w przypadku wielu serwerów Nginx (a tym samym pamięci podręcznych) tracisz możliwość wylogowania po stronie serwera, ponieważ nie możesz wyczyścić (wymuszając odświeżenie) pamięci podręcznej tokenów na wszystkich z nich, takich jak /auth/logoutrobi w moim przykładzie. Pozostało ci jedynie 5 milionów czasu trwania pamięci podręcznej tokenów, co zmusi twoje zapytanie do szybkiego zapytania i poinformuje Nginx, że żądanie zostało odrzucone. Częściowym obejściem jest usunięcie nagłówka tokena lub pliku cookie na kliencie podczas wylogowywania.

Wszelkie komentarze będą mile widziane i mile widziane!

mbarthelemy
źródło
Powinieneś otrzymywać o wiele więcej pozytywnych opinii! Bardzo pomocne, dzięki!
Gershon Papi
„Dodałem kilka dodatkowych ulepszeń, takich jak punkt końcowy wylogowania, który unieważnia token, zwracając 401 (który jest również buforowany przez Nginx), tak że jeśli użytkownik kliknie wylogowanie, token nie może być już używany, nawet jeśli nie wygasł. „ - To jest sprytne! , ale czy faktycznie umieszczasz token na czarnej liście również w backendie, aby w przypadku awarii pamięci podręcznej lub czegoś innego użytkownik nadal nie był w stanie zalogować się za pomocą tego konkretnego tokena?
gaurav5430
„Należy jednak pamiętać, że w przypadku wielu serwerów Nginx (a tym samym pamięci podręcznych) tracisz możliwość wylogowania po stronie serwera, ponieważ nie możesz wyczyścić (wymuszając odświeżenie) pamięci podręcznej tokenów na wszystkich z nich, tak jak / auth / logout robi w moim przykładzie. ” czy możesz rozwinąć?
gaurav5430