Budowa · Zaawansowane
Cache a ceny indywidualne klientów: gdy szybka strona pokazuje złą cenę
Cache stron zapisuje jedną odpowiedź i podaje ją wielu odwiedzającym. Jeśli ta odpowiedź zawiera indywidualną cenę klienta albo dane konta, dostaje je także następny odwiedzający.

Zdjęcie: Derrick Coetzee, domena publiczna (CC0)
W tym poradniku
Jak cache stron podaje cenę jednego klienta innemu
Cache zapisuje odpowiedź pod kluczem cache i zwraca zapisaną kopię przy każdym późniejszym żądaniu, które daje ten sam klucz. Domyślnie klucz powstaje z metody żądania i adresu URL: schematu, hosta, ścieżki i ciągu zapytania.
Weźmy sklep B2B, w którym klient A ma rabat umowny. A jest zalogowany i otwiera stronę produktu. Aplikacja odczytuje ciasteczko sesji i generuje stronę z ceną klienta A. Cache stojący przed aplikacją zapisuje tę odpowiedź pod kluczem adresu URL produktu. Klient B otwiera ten sam adres. Klucz jest identyczny, więc cache zwraca zapisaną stronę, z ceną klienta A i być może z nazwą jego firmy, nie pytając aplikacji.
Aplikacja policzyła właściwą cenę, a cache zrobił to, do czego go skonfigurowano. Błąd polega na tym, że odpowiedź zależała od czegoś, czego nie było w kluczu: od tego, kto pyta. Każdy taki wyciek wygląda tak samo. Odpowiedź zmienia się w zależności od jakiegoś wymiaru, na przykład klienta, cennika albo waluty, a w kluczu go brakuje.
Cache współdzielony i prywatny cache przeglądarki
- Cache współdzielony przechowuje odpowiedzi dla wielu użytkowników: CDN, reverse proxy przed serwerem WWW albo cache stron w samym serwerze lub w aplikacji.
- Cache prywatny należy do jednego użytkownika. W praktyce to cache przeglądarki.
Osobista odpowiedź w cache współdzielonym to wyciek między klientami. W cache przeglądarki zostaje u tego użytkownika, ale i tak może się pojawić po wylogowaniu, na przykład po kliknięciu przycisku „wstecz” na komputerze, z którego korzysta kilka osób.
HTTP ma jedno wbudowane zabezpieczenie: odpowiedzi na żądanie z nagłówkiem Authorization cache współdzielony nie może użyć ponownie, chyba że odpowiedź wyraźnie na to pozwala. Ciasteczka takiej ochrony nie mają. Dla cache ciasteczko sesji nic nie znaczy. Większość sklepów i portali trzyma logowanie w ciasteczku, więc zabezpieczenie musi wynikać z waszych własnych nagłówków i konfiguracji cache.
Cache stron wbudowany w aplikacje i moduły serwera często kieruje się własnymi regułami, na przykład „nie zapisuj, gdy jest ciasteczko logowania”. Jeśli wasze logowanie używa ciasteczka, którego cache nie zna, ta reguła nigdy nie zadziała.
Nagłówki HTTP, które decydują, kto może zapisać odpowiedź
Serwer źródłowy mówi cache, co im wolno, w nagłówku Cache-Control odpowiedzi.
- private: odpowiedź jest przeznaczona dla jednego użytkownika. Cache współdzielony nie może jej zapisać, przeglądarka może.
- public: odpowiedź może zapisać każdy cache, nawet taką, którą bez tego wolno byłoby przechowywać tylko prywatnie.
- no-store: żaden cache, współdzielony ani prywatny, nie powinien zapisywać odpowiedzi.
- no-cache: odpowiedź można zapisać, ale przed każdym ponownym użyciem trzeba ją zweryfikować u serwera źródłowego.
- s-maxage: przez ile sekund odpowiedź uchodzi za świeżą w cache współdzielonych, gdzie zastępuje max-age. Przeglądarki go ignorują.
Odpowiedź bez żadnej z tych dyrektyw nie jest automatycznie bezpieczna. Cache może sam oszacować czas świeżości, gdy serwer źródłowy go nie podaje, a wiele proxy i CDN-ów ma skonfigurowane reguły, które nadpisują nagłówki serwera źródłowego. Reguła „zapisuj wszystko”, dodana dla przyspieszenia sklepu, może sprawić, że poprawny nagłówek Cache-Control: private przestaje się liczyć.
Uważajcie także na Set-Cookie. Jeśli cache współdzielony zapisze odpowiedź, która ustawia ciasteczko sesji, mogą je dostać późniejsi odwiedzający. Odpowiedzi, które rozpoczynają albo zmieniają sesję, powinny mieć no-store.
Dlaczego Vary: Cookie to tępe narzędzie
Nagłówek odpowiedzi Vary wymienia nagłówki żądania, które stają się częścią klucza. Vary: Cookie wygląda więc na rozwiązanie: jedna kopia na każdą wartość ciasteczka.
Rzadko jest to dobre rozwiązanie. Cache porównuje cały nagłówek Cookie, a w nim są też ciasteczka analityki i zgód, inne u prawie każdego odwiedzającego. Anonimowi odwiedzający, którzy mogliby korzystać z jednej kopii, dostają każdy własną, a odsetek trafień gwałtownie spada. Strony osobiste nadal trafiają do cache współdzielonego, po jednej kopii na klienta. Do tego niektóre CDN-y obsługują Vary tylko dla wybranych nagłówków, więc przeczytajcie dokumentację swojego i przetestujcie.
Granice sesji: dzielcie stronę, a nie cache
Niezawodny projekt oddziela to, co takie samo dla wszystkich, od tego, co osobiste, i wysyła każdą część z własną polityką.
- Wygenerujcie stronę bez niczego osobistego: dane produktu, zdjęcia, opisy oraz cena katalogowa albo brak ceny. Wyślijcie ją z public i s-maxage, żeby cache współdzielony mógł ją podawać wszystkim.
- Części osobiste wczytajcie osobnym żądaniem, gdy strona już dotrze: cenę klienta, nazwę konta, liczbę produktów w koszyku. Te odpowiedzi wyślijcie z private, no-store.
- Przy każdym żądaniu osobistym sprawdzajcie sesję na serwerze.
Jeśli ceny zależą od cennika wspólnego dla wielu klientów, identyfikator cennika można dodać do klucza cache, żeby każda grupa miała własną kopię. Jest to bezpieczne tylko wtedy, gdy identyfikator pochodzi ze zweryfikowanej sesji. Zwykłe ciasteczko w rodzaju price_group=3, które odwiedzający może zmienić, pozwala każdemu pobrać zapisaną stronę innej grupy.
Poprawna jest też prostsza granica: omijajcie cache współdzielony przy każdym żądaniu z ciasteczkiem sesji. To się sprawdza, gdy zalogowani klienci to niewielka część ruchu. W ruchliwym systemie zamówień B2B, gdzie zalogowany jest prawie każdy odwiedzający, projekt z podziałem ma większe znaczenie.
Cache a ceny indywidualne klientów: polityka dla każdego typu strony
| Typ strony | Treść osobista | Cache współdzielony | Cache-Control w odpowiedzi |
|---|---|---|---|
| Wersjonowane pliki CSS, JavaScript, obrazy | Brak | Tak | public, max-age=31536000, immutable |
| Strony z treścią i katalogiem bez cen osobistych | Brak | Tak, czyszczenie przy zmianie | public, ustawione s-maxage, krótkie max-age |
| Szkielet strony produktu, cena wczytywana osobno | Brak w szkielecie | Tak | public, ustawione s-maxage, krótkie max-age |
| Żądanie ceny klienta albo liczby produktów w koszyku | Cena, koszyk | Nie | private, no-store |
| Strona produktu wygenerowana z ceną klienta | Cena | Nie | private, no-store |
| Koszyk, składanie zamówienia, konto, zamówienia, faktury | Wszystko | Nie | no-store |
| Logowanie, wylogowanie, każda odpowiedź, która ustawia ciasteczko sesji | Sesja | Nie | no-store |
Testujcie z dwoma klientami i anonimowym odwiedzającym
Tego wycieku nie widać, kiedy jeden programista testuje na jednym koncie. Do testu potrzeba dwóch klientów z różnymi cenami, a test musi przechodzić przez ten sam łańcuch cache co środowisko produkcyjne, a nie trafiać prosto do aplikacji.
- Wybierzcie produkt, na który klient A i klient B mają różne ceny. Wyczyśćcie cache dla jego adresu URL albo poczekajcie, aż wygaśnie.
- Otwórzcie stronę jako klient A. Zapiszcie pokazaną cenę, wszelkie dane konta i nagłówki odpowiedzi.
- Otwórzcie ten sam adres URL jako klient B z osobnego profilu przeglądarki albo klienta HTTP. Potem otwórzcie go bez żadnych ciasteczek.
- Porównajcie trzy odpowiedzi. B musi zobaczyć swoją cenę, a anonimowy odwiedzający nie może zobaczyć ceny klienta, nazwy konta ani cudzego ciasteczka sesji.
- Przeczytajcie nagłówki: Cache-Control, Vary, Set-Cookie i Age. Nagłówek Age w odpowiedzi osobistej oznacza, że pochodzi ona z cache. Wiele CDN-ów i proxy dodaje też nagłówek, który informuje o trafieniu albo chybieniu.
- Powtórzcie w zmienionej kolejności: najpierw anonimowo, potem A, potem B. Cache wypełnia pierwszy pytający, więc każda kolejność sprawdza inną awarię.
- Przeprowadźcie tę samą sekwencję na osobnych żądaniach ceny i koszyka, a potem wylogujcie się i użyjcie przycisku „wstecz” na stronach konta, żeby sprawdzić cache przeglądarki.
Dodajcie tę sekwencję do testów automatycznych, które działają przed każdym wydaniem, i uruchamiajcie ją ponownie po każdej zmianie ustawień CDN-u, proxy albo cache. Poprawa wydajności to moment, w którym ten błąd najłatwiej wprowadzić, a portale klientów wymagają tego samego testu co sklepy.
Pytania, które słyszymy często
Chcecie mieć to załatwione bez robienia tego samodzielnie?
Napiszcie, nad czym pracujecie. W ciągu jednego dnia roboczego odpowie wam człowiek.
