Preskoči na sadržaj

Baratanje HTTP kolačićima u jeziku PHP

HTTP kolačić (engl. HTTP cookie, Wikipedia, više detalja o kolačićima na MDN-u) je maleni dio podataka koji korisnički agent pohranjuje na računalu korisnika kod pregledavanja web sjedišta. Na temelju pohranjenih podataka web sjedište pamti stanje koje je korisnik stvorio svojim pregledavanjem, npr. predmete dodane u košaricu za kupnju, prijavu na zatvoreni dio sjedišta upotrebom određenog korisničkog računa ili tekst prethodnih pretraga arhive audiovizualnih datoteka.

Način rada kolačića

HTTP zaglavlje Set-Cookie je dio odgovora na zahtjev i koristi se za postavljanje kolačića koji se pohranjuju na klijentskoj strani (više detalja o HTTP zaglavlju Set-Cookie na MDN-u). Sadržaj zaglavlja Set-Cookie je oblika cookie-name=cookie-value, pri čemu je cookie-name ime kolačića, a cookie-value njegova vrijednost. Primjerice, kako bi poslužitelj pohranio na klijentu identifikator korisnika, odnosno kolačić imena id koji ima vrijednost 1234, zaglavlje Set-Cookie bit će oblika:

Set-Cookie: id=1234

Način pohrane kolačića je prepušten implementaciji pa cURL pohranjuje u tekstualnu datoteku, a Firefox u relacijsku bazu podataka. Kod slanja idućeg zahtjeva korisnički agent učitava pohranjene kolačiće i šalje ih u HTTP zaglavlju Cookie (više detalja o HTTP zaglavlju Cookie na MDN-u). U konkretnom primjeru s identifikatorom korisnika zaglavlje Cookie bit će oblika:

Cookie: id=1234

Postavljanje kolačića

Interpreter PHP-a podržava postavljanje kolačića funkcijom setcookie() (dokumentacija). Postavimo kolačić imena kolacic i vrijednosti Bugnes lyonnaises na način:

<?php

setcookie("kolacic", "Bugnes lyonnaises");

Kod korištenja cURL-a primljeni kolačići u odgovoru se pohranjuju u staklenku (engl. cookie jar) korištenjem parametra --cookie-jar, odnosno -c na način:

$ curl -v -c cookies.txt http://localhost:8000/
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 15:04:22 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
* Added cookie kolacic="Bugnes%20lyonnaises" for domain localhost, path /, expire 0
< Set-Cookie: kolacic=Bugnes%20lyonnaises
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0

Uočimo da u odgovoru postoji zaglavlje Set-Cookie koje postavlja kolačić pa sadrži njegov naziv i vrijednost. Ispišimo sadržaj stvorene staklenke cookies.txt:

$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   0       kolacic Bugnes%20lyonnaises

Poveznica u datoteci vodi nas na odjeljak HTTP Cookies u cURL-ovoj službenoj dokumentaciji koji objašnjava značaj pojedinih stupaca u svakom od redaka s kolačićem. U našem slučaju, u datoteci vidimo jedan postavljeni kolačić i možemo pročitati da taj kolačić:

  • na domeni localhost
  • ne odnosi se na poddomene
  • na putanji /
  • nije ograničen na HTTPS
  • ističe na kraju sesije (vrijednost 0)
  • ima ime kolacic
  • ima vrijednost Bugnes%20lyonnaises

Postavili smo ime i vrijednost kolačića, a ostale postavke kolačića (domena, odnosi li se na poddomene, putanja, ograničenost na HTTPS i vrijeme isteka) su postavljene na zadanu vrijednost. Želimo li postaviti neke druge vrijednosti za neke od tih postavki kolačića, to možemo napraviti korištenjem polja opcionalnih parametara funkcije setcookie(). Primjerice, kodom:

<?php

setcookie("kolacic", "Bugnes lyonnaises", ["expires" => 1759233600, "path" => "/slasticarna"]);

postavit ćemo da kolačić ističe u trenutku 1759233600 Unix vremena, odnosno 30. rujna 2025. u 12 sati po UTC-u i odnosi se na putanju /slasticarna.

Osim ova dva, u asocijativnom polju s opcijama dostupni su nam i ključevi:

  • "domain", kojim postavljamo domenu
  • "secure", kojim postavljamo da se kolačić šalje samo kad se koristi HTTPS
  • "httponly", kojim postavljamo da je kolačić dostupan samo preko HTTP-a
  • "samesite", kojim postavljamo ograničenje kolačića na domenu (vrijednost "Lax" postavlja labavo ograničenje, a vrijednost "Strict" postavlja strogo ograničenje)

Primanje kolačića

Polje $_COOKIE (dokumentacija) sadrži sve kolačiće primljene od strane klijenta. U tom polju ključevi su nazivi kolačića, a vrijednosti upravo njihove vrijednosti. Za ilustraciju, provjerimo funkcijom array_key_exists() (dokumentacija) postoji li u tom polju kolačić pod nazivom kolacic, a zatim, ako postoji, dohvatimo njegovu vrijednost i ispišimo je:

<?php

if (array_key_exists("kolacic", $_COOKIE)) {
    echo $_COOKIE["kolacic"];
}

U cURL-u se staklenka kolačića šalje u zahtjevu parametrom --cookie, odnosno -b. Iskoristimo ga na način:

$ curl -v -b cookies.txt http://localhost:8000/
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.72.0
> Accept: */*
> Cookie: kolacic=Bugnes%20lyonnaises
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Tue, 29 Dec 2020 15:09:00 GMT
< Connection: close
< X-Powered-By: PHP/8.0.0
< Content-Type: text/html; charset=UTF-8
<
Bugnes lyonnaises
* Closing connection 0

Uočimo u zahtjevu dodatno zaglavlje Cookie koje sadrži naziv i vrijednost poslanog kolačića.

Primjena kolačića

Zamislimo pojednostavljenu slastičarnu: kod svake narudžbe kolača od strane korisnika, isti će biti odabran slučajno kako ne bismo morali implementirati odabir kolača. Pritom želimo pamtiti koji kolač je korisnik ranije "odabrao" kako bismo mogli kasnije razviti funkcionalnost koja ga pita kako mu se svidio pa iskoristiti ocjenjivanje kolača od strane korisnika za prikaz top liste najbolje ocijenjenih i sl.

Želimo da web poslužitelj na putanju /kolaci prima zahtjeve metodom GET i metodom POST. Zahtjev metodom GET daje korisniku informacije o tome koji je kolač naručio u prethodnom koraku. Zahtjev metodom POST naručuje novi kolač.

Odaberimo par kolača s Wikipedijinog popisa i ponudimo ih. Gruba struktura programa je:

<?php

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis prethodno naručenog kolača
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // narudžba kolača slučajnim odabirom
    }
}

Pohrana podataka o korisnicima

Kako je HTTP protokol koji ne održava stanje (engl. stateless protocol), svaki zahtjev se obrađuje neovisno o prethodnima. Kako bismo spremili podatke o kolačima koje su korisnici naručili, trebat će nam datoteka. U tu datoteku u koju ćemo spremiti serijalizirane podatke iz memorije za kasnije učitavanje; ponovno ćemo iskoristiti serijalizaciju u oblik JSON funkcijom json_encode() te kod učitavanja deserializaciju iz JSON-a u podatke funkcijom json_decode().

U nastavku ćemo koristiti tri funkcije iz dijela Filesystem:

  • kod pokretanja ćemo funkcijom file_exists() (dokumentacija) provjeriti ako postoji datoteka sa spremljenim podacima od ranije,
  • učitat ćemo spremljene podatke iz datoteke u memoriju funkcijom file_get_contents() (dokumentacija),
  • nakon završetka obrade zahtjeva spremiti ćemo podatke iz memorije u datoteku file_put_contents() (dokumentacija).

Datoteku nazovimo orders.json pa imamo kod oblika:

<?php

$datoteka = "orders.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $orders = json_decode($j, true);
} else {
    $orders = [];
}

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        // ispis prethodno naručenog kolača
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // narudžba kolača slučajnim odabirom
    }
}

$j = json_encode($orders);
file_put_contents($datoteka, $j);

Dohvaćanje prethodno spremljenih podataka o korisniku na temelju kolačića

Kako bismo korisniku prezentirali informaciju o tome koji je kolač ranije naručio, iskoristit ćemo podatak user_id iz kolačića koji on pošalje za dohvaćanje identifikatora korisnika u polju $orders. Kod ispisa informacija o prethodnoj narudžbi u odgovoru iskoristili smo operator konkatenacije (znak točke, .) za spajanje znakovnih nizova.

Ako korisnik nije poslao kolačić, ili je poslao kolačić koji nema podatak user_id, ili je poslao kolačić čiji user_id ne postoji u polju, poslužitelj će mu vratiti odgovor da dosad nije naručio nijedan kolač sa statusnim kodom odgovora postavljenim na 404 Not Found.

<?php

$datoteka = "orders.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $orders = json_decode($j, true);
} else {
    $orders = [];
}

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        if (array_key_exists("user_id", $_COOKIE) && array_key_exists($_COOKIE["user_id"], $orders)) {
            $user_id = $_COOKIE["user_id"];
            $prethodni_kolac = $orders[$user_id];
            echo "<p>Ranije ste naručili " . $prethodni_kolac . ".</p>\n";
        } else {
        http_response_code(404);
        echo "<p>Dosad niste naručili nijedan kolač.</p>\n";
    }
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        // narudžba kolača slučajnim odabirom
    }
}

$j = json_encode($orders);
file_put_contents($datoteka, $j);

Uvjerimo se da dosad nismo naručili nijedan kolač:

curl -v http://localhost:8000/kolaci
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /kolaci HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Host: localhost:8000
< Date: Wed, 05 May 2021 19:50:41 GMT
< Connection: close
< X-Powered-By: PHP/7.4.15
< Content-Type: text/html; charset=UTF-8
<
<p>Dosad niste naručili nijedan kolač.</p>
* Closing connection 0

Uočimo kako je stvorena i datoteka public/orders.json sadržaja:

[]

Prazno polje indicira da dosad još nije bilo narudžbi.

Osvježavanje podataka o korisnikoj narudžbi

Kod naručivanja, odabir kolača ćemo izvesti slučajno među ponuđenima funkcijom array_rand() (dokumentacija) koja će slučajno odabrati ključ iz danog polja. Putem ključa dohvatit ćemo kolač.

Ako je korisnik poslao kolačić u kojemu je sadržan podatak user_id i taj identifikator postoji u polju u polju $orders, zamijenit ćemo zapis o prethodno naručenom kolaču novim (to činimo ovdje radi jednostavnosti; u praksi web aplikacije uglavnom dopunjavaju podatke, a vrlo rijetko brišu išta). Slično kao kod dohvaćanja podataka, ako korisnik nije poslao kolačić, ili je poslao kolačić koji nema podatak user_id, ili je poslao kolačić čiji user_id ne postoji u polju, poslužitelj će u polju $orders stvoriti podatke pod novim indeksom pa taj indeks taj indeks poslati korisniku u kolačiću pod user_id.

<?php

$datoteka = "orders.json";
if (file_exists($datoteka)) {
    $j = file_get_contents($datoteka);
    $orders = json_decode($j, true);
} else {
    $orders = [];
}

$kolaci = ["Babka", "Schwarzwälder Kirschtorte", "Kremówka", "Buccellato", "Kladdkaka"];

if ($_SERVER["REQUEST_URI"] == "/kolaci") {
    if ($_SERVER["REQUEST_METHOD"] == "GET") {
        if (array_key_exists("user_id", $_COOKIE) && array_key_exists($_COOKIE["user_id"], $orders)) {
            $user_id = $_COOKIE["user_id"];
            $prethodni_kolac = $orders[$user_id];
            echo "<p>Ranije ste naručili " . $prethodni_kolac . ".</p>\n";
        } else {
        http_response_code(404);
        echo "<p>Dosad niste naručili nijedan kolač.</p>\n";
    }
    } else if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $kolac_key = array_rand($kolaci);
    $kolac = $kolaci[$kolac_key];
        echo "<p>Naručili ste " . $kolac . ".</p>\n";
        if (array_key_exists("user_id", $_COOKIE) && array_key_exists($_COOKIE["user_id"], $orders)) {
            $user_id = $_COOKIE["user_id"];
            $orders[$user_id] = $kolac;
        } else {
            $orders[] = $kolac;
            $user_id = array_key_last($orders);
            setcookie("user_id", $user_id);
        }
    }
}

$j = json_encode($orders);
file_put_contents($datoteka, $j);

Isprobajmo navedeni kod:

$ curl -v -c cookies.txt -X POST http://localhost:8000/kolaci
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /kolaci HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Wed, 05 May 2021 20:02:58 GMT
< Connection: close
< X-Powered-By: PHP/7.4.15
* Added cookie user_id="0" for domain localhost, path /, expire 0
< Set-Cookie: user_id=0
< Content-Type: text/html; charset=UTF-8
<
<p>Naručili ste Babka.</p>
* Closing connection 0

Stvorena je datoteka cookies.txt sadržaja:

# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   0       user_id 0

Iskoristimo tu datoteku kod slanja idućeg zahtjeva:

$ curl -v -b cookies.txt http://localhost:8000/kolaci
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /kolaci HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.74.0
> Accept: */*
> Cookie: user_id=0
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Wed, 05 May 2021 20:03:05 GMT
< Connection: close
< X-Powered-By: PHP/7.4.15
< Content-Type: text/html; charset=UTF-8
<
<p>Ranije ste naručili Babka.</p>
* Closing connection 0

Možemo se uvjeriti da je stvorena datoteka orders.json koja sadrži listu:

["Babka"]

Warning

Iz sigurnosne perspektive, korištenje kratkih, predvidljivih i nepromjenjivih identifikatora korisnika u kolačićima kao što su redom brojevi 0, 1, 2, ... je loš pristup jer otvara puno prostora za napad. U praksi bi postavljanje kolačića bilo nešto složenije, ali ovakav pristup je procesu učenja sasvim dovoljan za ilustraciju načina postavljanja i dohvaćanja kolačića.

Isprobajmo naručivanje kolača kao korisnik koji je već ranije naručio kolač:

$ curl -v -b cookies.txt -X POST http://localhost:8000/kolaci
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /kolaci HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.74.0
> Accept: */*
> Cookie: user_id=0
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Wed, 05 May 2021 20:06:14 GMT
< Connection: close
< X-Powered-By: PHP/7.4.15
< Content-Type: text/html; charset=UTF-8
<
<p>Naručili ste Kremówka.</p>
* Closing connection 0

Uvjerimo se da je ova narudžba u datoteci orders.json zamijenila prethodnu:

["Krem\u00f3wka"]

Naručimo još jedan kolač kao novi korisnik:

$ curl -v -c cookies-new.txt -X POST http://localhost:8000/kolaci
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /kolaci HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.74.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8000
< Date: Wed, 05 May 2021 20:09:44 GMT
< Connection: close
< X-Powered-By: PHP/7.4.15
< Set-Cookie: user_id=1
< Content-Type: text/html; charset=UTF-8
<
<p>Naručili ste Kladdkaka.</p>
* Closing connection 0

Uočimo kako je u zaglavlju Set-Cookie u odgovoru postavljen identifikator novog korisnika na 1. Datoteka orders.json sad ima sadržaj:

["Krem\u00f3wka","Kladdkaka"]

Author: Vedran Miletić