Jak przechowywać dane w przeglądarce cz.2

Cover Image for Jak przechowywać dane w przeglądarce cz.2
Krzysztof
Krzysztof

W ostatnim wpisie z tej serii przejrzeliśmy, jakie możliwości oferują nam web storage api. Jeśli jeszcze nie znalazła się wolna chwila, aby go przeczytać, link znajdziesz tutaj

Dzisiaj na tapet weźmiemy kolejną opcję przechowywania danych w przeglądarce

Cookies

Ciasteczka to nic innego jak dane trzymane w przeglądarce w formie tekstowej. Mogą zostać utworzone zarówno za pośrednictwem JS’a, jak i za pomocą requestu HTTP z serwera.

Ten typ storage zdecydowanie różni się od poprzedników. Na pierwszy rzut oka ograniczenia są dużo większe, bo mamy do dyspozycji:

  • tylko 4kb pamięci
  • nie więcej niż 20 ciasteczek dla każdej domeny

Jednakże przy odpowiednim wykorzystaniu ciasteczka mogą być również najbezpieczniejszym frontendowym rozwiązaniem

Tworzenie cookies z poziomu JS’a

Dostęp do ciasteczek z poziomu JavaScript’a uzyskujemy za pośrednictwem klucza document.cookie. Zwróci on stringa oddzielonego średnikami gdzie każdy atrybut ciasteczka będzie stringiem klucz=wartość

Przykładowo:

document.cookie = „instytut=fullstack; domain=instytutfullstack.pl; Secure”

Utworzy nam ciasteczko o kluczu instytut z wartością fullstack oraz dwoma dodatkowymi atrybutami

Co ciekawe taki zapis nie nadpisze nam ciasteczek, jeśli byłyby już jakieś, a jedynie doda nowe lub zmodyfikuje, jeśli występują z tą samą nazwą.

Sterowanie ich zachowaniem oraz cechami za pomocą atrybutów

  • Domain

Tym atrybutem określamy, dla jakiej domeny ma być dostępne ciastko. Warto zaznaczyć, że gdy nie sprecyzujemy powyższego atrybutu i wykonamy

document.cookie = "author=Krzysztof"

Na domenie instytutfullstack.pl to utworzone w ten sposób ciasteczko nie będzie dostępne dla subdomen, czyli np. test.instytutfullstack.pl W celu umożliwienia takiego dostępu należy wyraźnie zdefiniować atrybut domain:

document.cookie = "author=Krzysztof; domain=instytutfullstack.pl"
  • Path Za pomocą path ustawiamy ścieżkę, dla której ciastko jest widoczne. W praktyce zwykle stosuje się path=/ żeby dane były dostępne na całej witrynie

  • Expires Pozwala ustawić datę, do której dane będą dostępne Bardzo ważne jest to, żeby ustawić ją w formacie UTC dla strefy czasowej GMT Przykład, jak dodać ciastko o "dacie przydatności do spożycia" 5 dni:

const date= new Date(); date.setDate(date.getDate() + 5); date.toUTCString() document.cookie = "user=John; expires=" + date;
  • Max-age Określa czas życia ciastka od momentu utworzenia. Jego wartość jest liczona w sekundach. Przykładowo zapis:
document.cookie = "author=Krzysztof; max-age=600";

Sprawi, że author=Krzysztof będzie dostępne przez 10 min od utworzenia

  • Secure Atrybut typu boolean oznaczający możliwość dostępu do danych tylko za pośrednictwem szyfrowanego protokołu https

  • HttpOnly Parametr używany z poziomu serwera. Jego dodanie oznacza, że kod JavaScript klienta, do którego serwer wysyła ciastko, nie będzie w stanie go odczytać (może być ono jedynie odesłane z powrotem na serwer)

  • SameSite Stosowany głównie ze względów bezpieczeństwa i posiada dwie możliwe wartości: strict oraz lax. To złożony temat wymagający omówienia w osobnym artykule, dlatego tutaj tylko krótko opiszemy, o co chodzi.

W telegraficznym skrócie opcja strict nie pozwala dodać ciastka do requestu który pochodzi z innej domeny (obrona przed atakiem XSRF)

Z kolei lax jest nieco bardziej luźne, bo mimo tego, że również stosuje mechanizm, jak strict to posiada pewne wyjątki w swej surowości. Mianowicie lax przymknie oko, gdy request należy do grupy "bezpiecznych metod HTTP (np. GET, HEAD, OPTIONS)" oraz został wysłany w wyniku operacji "top-level navigation", czyli np naciśnięcia przycisku wstecz w przeglądarce. Należy jednak pamiętać, że ten wyjątek nie dotyczy iframów

Z istotnych informacji, warto dodać, że co stanie się, jeżeli równocześnie dodamy atrybuty Expires oraz max-age. W przypadku przeglądarek wspierających Max-age tylko on będzie wzięty pod uwagę, a expires zostanie zignorowany.

Natomiast jeśli nie określimy czasu "życia" ciastka za pomocą żadnego z powyższych atrybutów, to zostanie ono usunięte po zamknięciu przeglądarki.

Jak łatwo zauważyć domyślne API do obsługi ciasteczek, nie jest zbyt przyjazne, więc polecamy skorzystać z jakiejś gotowej biblioteki jak np. js-cookie

Przykład użycia js-cookie:

Cookies.set('name', 'value', { expires: 7, path: '' })

Wykorzystując obiekty w JavaScript o wiele wygodniej nam sterować ciasteczkami. Zamiast "sklejać stringi" możemy dodawać klucze do obiektu

Tworzenie cookies na serwerze

Jesteśmy w stanie również stworzyć ciasteczko po stronie serwera i wysłać je w odpowiedzi na request http. Należy w tym celu użyć nagłówka "Set-Cookie", który sprawi, że ciasteczko zostanie zapisane po stronie klienta.

Istnieje także możliwość stworzenia ciasteczka, do którego nie będzie dostępu za pośrednictwem document.cookie, czyli z poziomu JS’a. Należy w tym celu dodać atrybut httpOnly do ciastka ustawianego za pośrednictwem wyżej wspomnianego Set-Cookie. Takie ciastko może być jedynie odesłane z powrotem na serwer.

Po co nam coś takiego?

Jednym z przykładów użycia, może być obsługa sesji użytkownika. Wysyłamy wtedy ciastko z serwera o kluczu np. sessionId i wartości sesji, a także atrybutem expires

W tym przypadku klient nie ma potrzeby dostępu do tych danych. Możemy więc ograniczyć ryzyko wycieku i zabronić wglądu do ciastka z poziomu kodu.

Usuwanie ciasteczka

Niestety nie ma dostępnej metody clear ani delete. Żeby usunąć ciasteczko, musimy posłużyć się trickiem i np. ustawić atrybut expires na datę wstecz, dzięki czemu ciasteczko "samo się usunie"

Zarówno atrybuty max-age, jak i expires, będą działać. Oto przykłady dla obu przypadków:

document.cookie = "author=Krzysztof; expires=Thu, 01 Jan 1970 00:00:00 GMT"

lub

document.cookie = "author=Krzysztof; max-age=0";

Third party cookies

Obcując z cookiesami z pewnością prędzej czy później zetkniesz się z nazwą Third-party cookies. Ciastko możemy nazwać third-party, gdy jest umieszczone przez inną witrynę niż strona odwiedzana bezpośrednio przez nas

Aby lepiej to zobrazować, posłużmy się przykładem z obrazkami. Wyobraź sobie, że na naszej stronie instytutfullstack.pl umieścilibyśmy jakiś baner służący do płatnej reklamy. Adres skąd może zostać pobrany to np. https://example.com/advertisement.jpg

cookies1

Wysyłając request GET w celu pobrania reklamy strona, która nam go udostępnia, może ustawić nagłówek Set-cookie i tym samym w odpowiedzi zwrócić ciasteczko z jakimś trackId

cookies2 cookies3

Łatwo zauważyć, że na ostatnim obrazku mamy do czynienia z sytuacją gdzie, nawet jeśli wchodzimy na inną witrynę korzystających z tych samych dostawców reklam ciastko, które zostało nam wysłane dalej może nas śledzić

Zobacz, wystarczy chwila nieuwagi, aby przez przypadek wejść na stronę, która utworzy third party cookie, a ono będzie podążać Twoim śladem w Internecie

Oczywiście third party cookies nie zawsze mają ten sam cel. Natomiast zdecydowana większość przypadków opiera się o potrzeby analityki lub reklam

Niektóre przeglądarki jak np. Safari domyślnie blokują takie zachowanie jednakże nie jest to reguła

Podsumowanie

Mamy nadzieję, że po tym wpisie o wiele bardziej świadomie będziesz stosować ciastka w swoich projektach. Jest to stary, lecz wciąż bardzo ważny element web developmentu. Poniżej umieszczamy kilka punktów podsumowujących, do których dla ułatwienia możesz wrócić i przypomnieć sobie, kiedy zastosować ciastka, a kiedy lepiej z nich zrezygnować

Warto zastosować ciastka:

  • Gdy chcemy używać zapisanych danych także po stronie serwera
  • Jeżeli znamy dokładną datę, w której chcemy się ciastka pozbyć
  • Jeśli chcemy zabezpieczyć dane przed dostępem z poziomu JS’a, ale nadal chcemy przechowywać je w przeglądarce

Lepiej zrezygnować z cookies:

  • W przypadku potrzeby przechowywania większej ilości informacji
  • Jeśli nie chcemy określać dokładnego momentu usunięcia danych
  • Do przechowywania wrażliwych informacji takich jak dane osobowe itp.

Na sam koniec przykład z życia wzięty: W ciastku httpOnly + secure + SameSite trzymamy tokeny JWT lub informacje o sesji obsługiwanej przez backend