kleindan.dev

Przechowywanie danych w przeglądarce - cookies

Rozpoczynamy serię lekcji, w których ostatecznie zajmiemy się logowaniem użytkownika i nadawaniem mu odpowiednich uprawnień w aplikacji, ale zanim tam dojdziemy, musimy przejść przez kilka kroków. Pierwszym krokiem będzie poznanie dość starego, ale wciąż używanego mechanizmu przechowywania danych po stronie przeglądarki użytkownika.

Wideo towarzyszące

Czym są cookies i jak działają?

Cookies to krótkie informacje tekstowe, które serwer wysyła do przeglądarki, a przeglądarka następnie odsyła te informacje do serwera w kolejnych żądaniach.

Konkretnie, w odpowiedzi na żądanie HTTP serwer może umieścić w odpowiedzi nagłówek w takiej postaci:

Set-Cookie: <nazwa>=<wartosc>

Czyli jeżeli, na przykład, nasza aplikacja będzie oferować różne schematy kolorystyczne, jasny i ciemny, a użytkownik wybierze przełączenie aplikacji w ciemniejszy schemat, w odpowiedzi serwer może odesłać w odpowiedzi nagłówek:

HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: colorscheme=dark

[dokument html]

W takim przypadku przeglądarka użytkownika wysyłając kolejne żądanie dopisze do niego dodatków nagłówek w takiej postaci:

GET /jakas/sciezka HTTP/2.0
Host: example.com
Cookie: colorscheme=dark

W ten sposób nasz serwer będzie wiedział, żeby serwować użytkownikowi strony w odpowiedniej palecie kolorystycznej.

Alternatywnie moglibyśmy umieścić to ustawienie np. w linkach jako query parameter, tj. ?colorscheme=dark ale to miałoby kilka wad: po pierwsze, musielibyśmy pamiętać o dodaniu odpowiedniego stringu do wszystkich linków generowanych na stronie, a po drugie w ten sposób jeżeli nasz użytkownik wysłałby link do naszej strony do znajomego, automatycznie ustawilibyśmy dla nowo przybyłego użytkownika taki sam schemat kolorystyczny. Nie jest to poważna wada, ale pokazuje co trzeba rozważać, kiedy podejmujemy decyzję o tym, gdzie przechowywać daną informację.

Typowe zastosowania cookies

Najczęstsze zastosowania cookies to:

Istnieją bardziej niszowe zastosowania cookies i może do nich jeszcze wrócimy, dziś skupimy się na pierwszym zastosowaniu. W zależności od zastosowania naszej aplikacji, możemy oferować użytkownikowi różne ustawienia. Może być to język wyświetlanej strony, waluta w której pokazujemy ceny, potencjalny kraj do wysyłki towarów albo strefa czasowa, w której przebywa nasz użytkownik i wiele, wiele innych.

Innym zastosowaniem z tej kategorii jest przechowywanie informacji np. o ostatnio przeglądanych podstronach naszej aplikacji. W przypadku aplikacji do fiszek możemy sobie wyobrazić sytuację, w której mamy w aplikacji dziesiątki kategorii, ale możemy podsunąć użytkownikowi pod nos np. 5 ostatnio przez niego odwiedzanych, ponieważ jest spore prawdopodobieństwo, że obecnie próbuje opanować jakiś konkretny materiał i może wracać do konkretnych zestawów fiszek.

W przypadku aplikacji typu sklep internetowy możemy pokazywać użytkownikowi ostatnio oglądane produkty albo inne z tej samej kategorii.

Jednym z podstawowych zastosowań analitycznych jest monitorowanie “nowych” odwiedzających, ponieważ jeżeli nasza aplikacja ustawi jakieś cookie domyślnie dla każdego odwiedzającego, to żądanie przychodzące bez takiego cookie prawdopodobnie pochodzi od nowego użytkownika. Niestety tylko prawdopodobnie, ponieważ użytkownicy mają pełną kontrolę nad cookies przechowywanymi w ich przeglądarce i mogą je bez naszej wiedzy usunąć albo użyć np. programu curl albo trybu incognito i w ten sposób zaburzyć nasze statystyki.

Jak długo żyją cookies?

Cookie ustawione przez serwer przy pomocy samego nagłówka Set-Cookie będzie przechowywane w przeglądarce do czasu zamknięcia danej zakładki lub okna. Jeżeli chcemy żeby dane w cookie przetrwały zamknięcie przeglądarki, musimy dodatkowo podać, jak długo dane pozostają ważne. Możemy to zrobić na dwa sposoby, dodając do cookie parametr Max-Age albo Expires. Używając poprzedniego przykładu, nasze cookie colorscheme mogłoby być ustawione na takie sposoby:

Set-Cookie: colorscheme=dark; Expires=Sat, 10 Jan 2026 10:11:12 GMT;
Set-Cookie: colorscheme=dark; Max-Age=2592000

W pierwszym przypadku podajemy dokładną datę i godzinę wygaśnięcia danych, po których przeglądarka usunie cookie i przestanie je wysyłać do serwera. W drugim przypadku podajemy czas w milisekundach do momentu wygaśnięcia cookie. O ile druga opcja jest mniej czytelna na pierwszy rzut oka, o tyle eliminuje problem w różnicach ustawieniach dat pomiędzy serwerem i przeglądarką.

Modyfikowanie cookies

Modyfikowanie cookies odbywa się poprzez ponowne ustawienie cookie o tej samej nazwie i parametrach z uwzględnieniem nowej wartości. W ten sposób możemy też przedłużać ważność cookie, ustawiając mu tę samą wartość, ale wysyłając nową datę wygaśnięcia cookie.

Usuwanie cookies

Jeżeli nasz serwer ustawił jakieś cookie w przeglądarce użytkownika, ale chciałby je usunąć, np. w celu wylogowania użytkownika, to powinien w odpowiedzi na żądanie użytkownika wysłać nagłówek Set-Cookie z podaną wcześniej nazwą oraz parametrem Expires ustawionym w przeszłości albo parametrem Max-Age o wartości zero lub negatywnej. Nie jest to najbardziej intuicyjny interfejs, ale jeżeli koniecznie potrzebujemy usunąć jakieś konkretne cookie, to nie mamy innego wyboru.

W przyszłych lekcjach omówimy inne strategie usuwania cookies z przeglądarki ze strony serwera.

Gdzie znaleźć więcej informacji

Większość informacji i wiele więcej znajdziecie w przewodniku MDN poświęconemu cookies oraz czytając dodatkowe informacje do których ten przewodnik linkuje.

Wymagania prawne związane z cookies

Ważne Nie jestem prawnikiem. Jeżeli potrzebujesz pewnej informacji z zakresu prawnego, skonsultuj się z odpowiednim specjalistą.

To powiedziawszy, zarówno Unia Europejska jak i część Stanów Zjednoczonych Ameryki wprowadziła obwarowania prawne, które ograniczają swobodne korzystanie z cookies przez programistów tworzących aplikacje internetowe.

Podstawowe przesłanie tych dokumentów prawnych jest dość proste. Użytkownik musi:

Co to znaczy niezbędne dla działania aplikacji? Tu zaczynają się schody. Pewnie każdy się zgodzi, że sklep internetowy musi mieć możliwość przechowywania informacji o produktach zebranych w koszyku użytkownika w celu obsłużenia jego zakupów. Być może to samo powiemy o preferencjach językowych na stronie hotelu. Ale czy możliwość wybrania odpowiedniego do ekranu schematu kolorystycznego to nadal niezbędne do działania dane? Osobiście uważam, że tak bo jest to kwestia dostępności aplikacji dla wszystkich użytkowników, ale pewnie znajdą się głosy sprzeciwu. W każdym razie jedno pozostaje pewne: jeżeli korzystamy z cookies musimy poinformować o tym użytkownika i mieć kontrolę nad tym, na co użytkownik się zgadza, a na co nie.

Jeżeli jesteś zainteresowanx, to polecam stronę GDPR poświęconą tym wymaganiom prawnym, gdzie autorzy dość prostym językiem tłumaczą, jakie obowiązki spoczywają na autorach stron internetowych.

Przykład użycia cookies w naszej aplikacji

W repozytorium z kodem do naszych lekcji znajduje się folder dedykowany dla aktualnego materiału, a w nim kolejne podfoldery:

01_start

Jako punkt wyjścia, zawierający kod aplikacji z końca poprzedniej lekcji.

02_cookies_theme

Implementacja wyboru pomiędzy jasnym i ciemnym tematem kolorystycznym aplikacji.

W tym wypadku musiałem przerobić sporo elementów aplikacji:

Najważniejsze zmiany jednak zawierają się w czterech plikach:

> index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import express from "express";
import morgan from "morgan";
import cookieParser from "cookie-parser";

import flashcards from "./models/flashcards.js";
import settings from "./models/settings.js";

const port = 8000;

const app = express();
app.set("view engine", "ejs");
app.use(express.static("public"));
app.use(express.urlencoded());
app.use(morgan("dev"));
app.use(cookieParser());

const settingsRouter = express.Router();
settingsRouter.use("/toggle-theme", settings.themeToggle);
app.use("/settings", settingsRouter);

function settingsLocals(req, res, next) {
  res.locals.app = settings.getSettings(req);
  res.locals.page = req.path;
  next();
}
app.use(settingsLocals);

app.get("/", (req, res) => {
    // Reszta pliku bez istotnych zmian
}
> views/partial.head.ejs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html data-theme="<%- app.theme %>" lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= title %></title>
    <link rel="stylesheet" href="/css/style.css" />
  </head>
  <body>
    <div class="container">
    <header>
    <nav>
      <% if (page !== "/") { %>
      <a href="/">Powrót do listy kategorii</a>
      <% } %>
    </nav>
    <section class="controls">
      <a href="/settings/toggle-theme?next=<%- encodeURIComponent(page) %>">
        <% if (app.theme === "dark") { %>
        <strong>Ciemny</strong> | Jasny
        <% } else { %>
        Ciemny | <strong>Jasny</strong>
        <% } %>
      </a>
    </section>
    </header>

Dodatkowa zagadka: gdyby zmienna app istniała, ale nie miała właściwości theme, jaki temat uznajemy za domyślny? Jasny czy ciemny?

> models/settings.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"use strict";

const ONE_DAY = 24 * 60 * 60 * 1000;
const THEME_COOKIE = "fisz-theme";

export function themeToggle(req, res) {
  var theme = req.cookies[THEME_COOKIE];
  if (theme === "dark") {
    theme = "light";
  } else {
    theme = "dark";
  }
  res.cookie(THEME_COOKIE, theme);

  var next = req.query.next || "/";
  res.redirect(next);
}

export function getSettings(req) {
  const settings = {
    theme: req.cookies[THEME_COOKIE] || "light",
  };
  return settings;
}

export default {
  themeToggle,
  getSettings,
};
> public/css/style.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
html {
    --backdrop: #830;
    --background: #ded;
    --text-color-normal: #222;
    --text-color-link: #44e;
    background-color: var(--backdrop);
    color: var(--text-color-normal);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    font-size: 11pt;
}

html[data-theme='dark'] {
    --backdrop: #510;
    --background: #483232;
    --text-color-normal: #ddd;
    --text-color-link: #77f;
}

W pliku CSS definiujemy kolory używane w aplikacji jako zmienne CSS. Pierwsza definicja będzie używana domyślnie. Druga, bardziej specyficzna zostanie użyta tylko wtedy, kiedy element html będzie miał atrybut data-theme ustawiony na wartość dark.

Implementacja banneru pozwalającego użytkownikowi wyrażenie zgody na użycie wszelkich cookies bądź odrzucenie użycia nieobowiązkowych danych. Implementacja bazuje na rozwiązaniu opublikowanym na tej stronie.

Przeanalizuj samodzielnie zmiany w plikach:

04_last_viewed_feature

Dodanie do naszej aplikacji wyświetlania listy ostatnio przeglądanych kategorii fiszek z uwzględnieniem faktu, że użytkownik mógł się na tę funkcjonalność nie zgodzić.

Najważniejsze zmiany sprowadzają się do dwóch plików:

> index.js
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
app.get("/", (req, res) => {
  var last_viewed_categories = null;
  if (res.locals.app.cookie_consent && req.cookies[LAST_VIEWED_COOKIE]) {
    let last_viewed = req.cookies[LAST_VIEWED_COOKIE]?.split(",") || [];
    last_viewed_categories = last_viewed
      .map((x) => parseInt(x, 10))
      .filter((x) => !isNaN(x))
      .map((id) => flashcards.getCategorySummary(id));
  }
  res.render("categories", {
    title: "Kategorie",
    categories: flashcards.getCategorySummaries(),
    last_viewed_categories,
  });
});

app.get("/view/:category_id", (req, res) => {
  const category = flashcards.getCategory(req.params.category_id);
  if (category != null) {
    if (res.locals.app.cookie_consent) {
      let last_viewed_dirty = req.cookies[LAST_VIEWED_COOKIE]?.split(",") || [];
      let last_viewed = [
        category.category_id,
        ...last_viewed_dirty
          .map((x) => parseInt(x, 10))
          .filter((x) => !isNaN(x) && x !== category.category_id)
          .slice(0, 2),
      ];
      res.cookie(LAST_VIEWED_COOKIE, last_viewed.join(","));
    }
    res.render("category", {
      title: category.name,
      category,
    });
  } else {
    res.sendStatus(404);
  }
});

Wyświetlanie listy jest obsłużone w szablonie EJS warunkowo:

> views/categories.ejs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<%- include("head.partial.ejs") %>

<main>
  <h1><%= title %></h1>
  <% if (last_viewed_categories?.length > 0) { %>
  <h2>Ostatnio przeglądane</h2>
  <ul id="recently_viewed">
    <% last_viewed_categories.forEach(function(category){ %>
    <li><a href="/view/<%= category.id %>"><%= category.name %></a></li>
    <% }) %>
  </ul>
  <h2>Wszystkie</h2>
  <% } /* (last_viewed_categories.length > 0) */ %>
  <ul id="categories">
    <% categories.forEach(function(category){ %>
    <li><a href="/view/<%= category.id %>"><%= category.name %></a></li>
    <% }) %>
  </ul>
  <a href="/new_category">Dodaj nową kategorię</a>
</main>

<%- include("foot.partial.ejs") %>

Bezpieczeństwo cookies

Dokładniej temat bezpieczeństwa cookies i związanych z nimi zagrożeń omówimy sobie na następnej lekcji. Teraz podkreślę pierwszą zasadę związaną z cookies dla nas jako programistów aplikacji webowych: cookies są danymi przychodzącymi z zewnątrz i należy je traktować adekwatnie: jak skażone, radioaktywne odpady, póki nie zweryfikujemy, że zawierają oczekiwane przez nas informacje. Użytkownicy mają pełną kontrolę nad danymi w swojej przeglądarce i tak powinno być. Ale dla nas to oznacza, że mogą je dowolnie modyfikować, usuwać i dodawać, więc pamiętaj: cookies podlegają walidacji i weryfikacji przed użyciem jak wszystkie inne dane z zewnątrz. Dodatkowo miejmy na uwadze, że żądania do naszego serwera nie muszą pochodzić z przeglądarki, nie raz korzystaliśmy z narzędzia curl do sprawdzania naszej aplikacji i tam również użytkownik ma pełną kontrolę nad zawartością wysyłanych cookies. Musimy mieć się na baczności.

Zadanie praktyczne

Ścieżka /settings/manage-cookies obsługiwana przez funkcję manageCookies z pliku models/settings.js jest właściwie nieobsłużona. Dopisz własną obsługę tej ścieżki pozwalającą co najmniej na zmianę zgody na zastosowanie cookies przez użytkownika. W przypadku cofnięcia zgody, pamiętaj aby usunąć cookies, które nie są niezbędne dla funkcjonowania aplikacji.