Pudełko z ciasteczkami

blog.comandeer.pl 1 dzień temu

Relacja Sieci z cookies (ciasteczkami) od prawie samego początku była mocno skomplikowana. Powstały jako wynalazek Netscape’a, żeby przechowywać krótkie informacje po stronie przeglądarki. Potem zaczęły być używane do śledzenia osób przeglądających Internet. W końcu pojawiły się ciasteczkowe prawa i dzisiaj praktycznie każda strona ma informację o wykorzystaniu ciasteczek. Niemniej nie tylko na polu prawno-etycznym ciasteczkom było trudno.

Podstawy działania ciasteczek, czyli stary przepis babci

Lata 90. to był dziwny czas dla platformy sieciowej. Jej kształt dopiero się wykuwał i sporo w tej chwili standardowych technologii wtedy choćby nie istniało. Same ciasteczka pojawiły się w 1994, więc na rok przed JavaScriptem! Z tego też względu oryginalna propozycja Netscape’a z oczywistych względów nie wspomina o żadnym JS-owym API. Zamiast tego jedynym sposobem ustawienia ciasteczek był nagłówek HTTP Set-Cookie:

Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure

Jego poszczególne części to:

  1. NAME – nazwa ciasteczka,
  2. VALUE – wartość ciasteczka
  3. expires=DATE – data ważności, po której ciasteczko zostanie usunięte; format daty opisany jest w standardzie RFC 2616,
  4. path=PATH – ścieżka, dla której ciasteczko ma być używane,
  5. domain=DOMAIN_NAME – nazwa domeny, dla której ciasteczko ma być używane,
  6. secure – słowo kluczowe, oznaczające, iż ciasteczko ma być wysyłane tylko bezpiecznym połączeniem (HTTPS).

Obowiązkowe było podanie tylko nazwy i wartości, resztę części można było ominąć.

Założenie ciasteczek było proste: serwer wysyła do przeglądarki jakąś informację (np. identyfikator koszyka w sklepie). Gdy dana strona zostanie potem ponownie odwiedzona, przeglądarka odsyła tę informację z powrotem. Robiła to przy pomocy nagłówka Cookie, dodawanego do żądania HTTP:

Cookie: cookie1=wartosc; cookie2=wartosc; cookie3=wartosc

Przeglądarka wysyłała jedynie nazwy i wartości istniejących ciasteczek. Każda para <nazwa, wartość> była oddzielona od siebie średnikiem. Tym sposobem można powiązać konkretną osobę (przeglądarkę) z konkretnymi danymi po stronie serwera.

Co ciekawe, nagłówek HTTP Set-Cookie niemalże w niezmienionej postaci przetrwał do dziś. Oficjalna specyfikacja ciasteczek, RFC 6265, definiuje go następująco:

Set-Cookie: NAME=VALUE; Expires=DATE; Max-Age=MAX_AGE; Domain=DOMAIN_NAME; Path=PATH; Secure; HttpOnly; EXTENSIONS

Tak naprawdę pojawiły się trzy nowe elementy:

  1. Max-Age=MAX_AGE – liczba sekund, po której ciasteczko traci ważność,
  2. HttpOnly – słowo najważniejsze wskazujące, iż ciasteczko ma być dostępne wyłącznie w żądaniach i odpowiedziach HTTP, bez dostępu z poziomu JS-a,
  3. EXTENSIONS – dodatkowe elementy nagłówka, opisane w innych specyfikacjach.

Na ten moment istnieją tak naprawdę dwa ustandaryzowane dodatkowe elementy nagłówka:

  1. Partitioned – słowo najważniejsze wskazujące, iż ciasteczko ma zostać wrzucone do osobnego słoika, dzięki czemu nie będzie mogło być użyte do śledzenia przeglądarki między różnymi stronami; część tzw. CHIPS (Cookies Having Independent Partitioned State, Ciasteczka Posiadające Niezależny Podzielony Stan),
  2. SameSite=None|Lax|Strict – określa, jak ciasteczko ma się zachowywać w żądaniach pomiędzy różnymi stronami.

Dygresja

Nie będę tutaj wchodził w szczegóły tych dwóch rozszerzeń, ponieważ zasługują tak naprawdę na osobne artykuły. W tym artykule w zupełności wystarczy nam wiedza, iż po prostu istnieją.

Nawet jednak biorąc pod uwagę cztery nowe elementy nagłówka Set-Cookie, można spokojnie uznać, iż podstawy działania ciasteczek pozostały niezmienne od ponad 30 lat. To wciąż są małe fragmenty informacji, które serwer może zapisać w przeglądarce, a przeglądarka odeśle je z powrotem przy każdym żądaniu HTTP.

Stare API, czyli wyżerając surowe ciasto

Wraz z pojawieniem się JS-a powstała potrzeba odczytywania i ustawiania ciasteczek z jego poziomu. Powstała zatem własność document.cookie, Za jej pomocą można zarówno odczytywać, jak i zapisywać ciasteczka.

Żeby zapisać ciasteczko, ustawiamy wartość własności document.cookie na poprawną zawartość nagłówka HTTP Set-Cookie:

document.cookie = `myCookie=wartosc;Expires=${ ( new Date( '2030-12-31' ) ).toUTCString() }`;

Powyższy kod stworzy nowe ciasteczko o nazwie myCookie i wartości wartosc. Ciasteczko będzie ważne do 31 grudnia 2030 roku. Warto zwrócić uwagę, iż wykorzystaliśmy tutaj metodę Date#toUTCString() – tworzy ona datę w formacie zgodnym ze standardem RFC 7231. Ogólnie można uznać, iż to ten sam format, który jest akceptowany w nagłówku Set-Cookie.

Dygresja

W przypadku standardów opisywanych przez RFC, nowsze RFC nadpisują starsze RFC. Tym samym format daty został już wielokrotnie nadpisany. Po raz pierwszy pojawia się w RFC 822, potem w RFC 1123, RFC 2616, RFC 2822, RFC 5322 i w końcu – RFC 7231. Na całe szczęście, zachodziły w nim głównie kosmetyczne poprawki oraz ujednolicanie różnych standardów. Oczywiście, ujednolicanie w ramach istniejących RFC – bo ten standard wciąż jest zupełnie inny niż standard ISO 8601.

Żeby odczytać istniejące ciasteczka, można odczytać wartość własności document.cookie:

console.log( document.cookie );

Zwróci to ciąg tekstowy w postaci:

cookie1=wartosc; cookie2=wartosc

Innymi słowy: jest to taki sam format, jaki ma nagłówek HTTP Cookie.

Stare JS-owe API ciasteczkowe jest, cóż, proste. I na tym jego lista zalet się kończy. Ustawianie zmiennej na wartość A, by następnie odczytać z niej wartość B samo w sobie brzmi jak słaby projekt API. Do tego dochodzą zwracanie ciasteczek w formie jednego, długiego ciągu znaków, który trzeba samemu parsować, oraz synchroniczność samego API. A mimo to przez lata nie doczekaliśmy się niczego sensowniejszego.

Nowe API, czyli przechowując ciasteczka w słoiku

Dopiero całkiem niedawno doczekaliśmy się nowego API, Cookie Store. Składa się ono z dwóch zasadniczych części: CookieStore oraz CookieStoreManager.

Dygresja

Tak, też uważam, iż to powinno się nazywać Cookie Jar API!

Przy ustawianiu i odczytywaniu ciasteczek potrzebny będzie nam CookieStore. Każda strona jest już w niego wyposażona, jako własność window.cookieStore:

await cookieStore.set( 'testowe', 'jakaś wartość' ); // 1 await cookieStore.set( { // 2 name: 'testowe2', value: 'ęą', sameSite: 'strict' } ); console.log( await cookieStore.getAll() ); // 3 await cookieStore.delete( 'testowe' ); // 4 console.log( await cookieStore.get( 'testowe' ) ); // 5

Na sam początek dodajemy przy pomocy metody CookieStore#set() nowe ciasteczko o nazwie testowe i wartości jakaś wartość (1). jeżeli chcemy podać więcej opcji przy tworzeniu ciasteczka, można przekazać do metody #set() obiekt z opcjami (2). Akceptowana jest większość opcji z nagłówka HTTP Set-Cookie, oprócz Secure (bo Cookie Store API i tak wymaga HTTPS) i HttpOnly (bo ciasteczka dostępne wyłącznie przez HTTP nie mają sensu w JS-ie). Następnie przy pomocy metody #getAll() wyświetlamy wszystkie zapisane ciasteczka (3). Potem, przy pomocy metody #delete() usuwamy ciasteczko testowe (4) i próbujemy je wyświetlić przy pomocy metody #get() (5). Z racji tego, iż ciasteczka już nie ma, w konsoli pojawi się wartość null. Warto zwrócić przy okazji uwagę, iż wszystkie metody CookieStore są asynchroniczne.

Na obiekcie CookieStore może też zajść zdarzenie change. Odpala się ono w momencie, gdy zajdzie jakakolwiek zmiana w ciasteczkach:

cookieStore.addEventListener( 'change', ( { changed, deleted } ) => { // 1 console.group( 'CookieChangeEvent' ); // 4 console.log( 'Zmienione ciasteczka', changed ); // 2 console.log( 'Usunięte ciasteczla', deleted ); // 3 console.groupEnd( 'CookieChangeEvent' ); // 5 } );

Nasłuchujemy na zdarzenie change (1). Gdy zajdzie, wyświetlamy informacje o nim: listę zmienionych ciasteczek (2) oraz listę usuniętych ciasteczek (3). Dzięki grupowaniu w konsoli (4, 5) informacje o konkretnym zdarzeniu są ładnie oddzielone od reszty komunikatów w konsoli:

Przykładowe zdarzenia change

Z kolei CookieStoreManager pozwala wykrywać zmiany w ciasteczkach z poziomu Service Workera. Nie umożliwia jednak modyfikowania ciasteczek.

Jeśli chodzi o CookieStore, wszystkie najważniejsze przeglądarki go wspierają. Z kolei CookieStoreManager nie jest obsługiwany w Safari.

Testowanie, czyli pora na degustację

Wypada jeszcze słowo poświęcić temu, w jaki sposób można testować ciasteczka. Tutaj na pomoc przychodzą narzędzia deweloperskie przeglądarki (devtools). W przypadku Chrome interesuje nas zakładka Application. W niej z kolei znajduje się sekcja Storage, a w niej – opcja Cookies. Po wejściu w nią, naszym oczom ukaże się lista zapisanych w przeglądarce ciasteczek dla danej strony:

Widok ciasteczek w Chrome

Z poziomu tego widoku można dodawać, modyfikować i usuwać ciasteczka. Co więcej, wszystkie zmiany wprowadzone tutaj zostaną wyłapane przez zdarzenie change obiektu CookieStore. Dzięki temu można przetestować, czy nasza logika obsługi ciasteczek działa poprawnie.

Z kolei w przypadku Firefoksa analogiczny widok jest dostępny w zakładce Storage w devtools:

Widok ciasteczek w FIrefoksie

Tutaj też można dodawać, usuwać i modyfikować ciasteczka.

Zatem nie przedłużając już więcej: miłej degu… znaczy testowania ciasteczek!

Idź do oryginalnego materiału