Preskočite 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). 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).

Postavljanje kolačića

Interpreter PHP-a podržava postavljanje kolačića funkcijom setcookie() (dokumentacija) 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.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   0       kolacic Bugnes%20lyonnaises

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

Podsjetimo se da je HTTP protokol koji ne održava stanje (engl. stateless) pa se svaki zahtjev obrađuje neovisno o prethodnima.

Kako bismo pohranili kolače koje su korisnici naručili, svakako će nam trebati datoteka u koju ćemo pohraniti serijalizirane podatke pohraniti funkcijom file_put_contents(). Ponovno ćemo iskoristiti serijalizaciju u oblik JSON funkcijom json_encode() te deserializaciju iz JSON-a u podatke funkcijom json_decode(). Kod pokretanja ćemo funkcijom file_exists() (dokumentacija) provjeriti ako postoji datoteka sa spremljenim podacima od ranije te ih učitati funkcijom file_get_contents(). 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ć