Sessions

Autor: Alphard
Session nám umožňují sdílet údaje mezi jednotlivými http requesty a identifikovat uživatele. Vysvětlíme si, jak pracují, podíváme se, k čemu se dají využít, a uvedeme i některé pokročilejší techniky, jak s nimi pracovat.

Princip


HTTP protokol je bezestavový, server vždy klientovi odpovídá výhradně na základě jeho http requestu (tj. žádost o konkrétní stránku). Aby bylo možné vytvářet složitější aplikace, je nezbytná přítomnost mechanismu sdílejícího určité informace mezi jednotlivými http požadavky.
Po zobecnění všech potřeb zjistíme, že nám stačí správně identifikovat opětovné přístupy od téhož uživatele. Samotná data přiřazená ke konkrétnímu uživateli již není třeba mezi stránkami sdílet, ale lze je ukládat na server a odkazovat se na ně.
Na tomto principu jsou založeny sessions. Při jejich vytváření je uživateli vygenerován unikátní identifikátor sloužící k jeho rozpoznání a umožňující přistupovat ke konkrétnímu datovému souboru. Pro sdílení mezi jednotlivými stránkami tento identifikátor uložíme do cookie, kterou prohlížeč na server odesílá při každém načítání stránky. Data samotná jsou defautně uložena v serializované podobě v souboru uloženém na serveru a pojmenovaném podle uvedeného identifikátoru.

Příklad http requestu

GET /wiki/Wikipedie HTTP/1.1
Host: cs.wikipedia.org
Accept-Charset: UTF-8,*
GET /programujeme-v-php/sessions?rev=1 HTTP/1.1
Host: pehapko.cz
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en,cs;q=0.8
Cookie: PHPSESSID=9g0jg1mgs26c1i0e4hok7rgbd1; nette-browser=spx2kdfq2y
Zde je uveden výběr položek, které jsou pro nás nyní zajímavé. V prvním případě vidíme zcela obecnou hlavičku, takhle může nějakou stránku žádat kdokoliv a server mu odpoví jako anonymnímu uživateli.
V druhém případě již hlavička obsahuje záznamy o cookies, vidíme tady hned dvě. Cookie s název nette-browser nás moc nezajímá, ale cookie PHPSESSID je klíčem k identifikaci „uživatele“ 9g0jg1mgs26c1i0e4hok7rgbd1.

Přenos session identifikátoru v URL
V jiných dílech učebnice jste viděli, že různé informace lze mezi stránkami přenášet jejich opakovaným vkládáním do URL, cookies je v principu méně viditelná alternativa k této metody sdílení dat (viz ukázka http requestu, všechna data jsou uvedena v textu požadavku).
Principiálně bychom při negativním nastavení use_only_cookies mohli session identifikátor přenášet v URL, z bezpečnostních důvodů to ale není rozumné; neboť bychom sdílením URL stránky vyzrazovali do světa náš session identifikátor. Všichni návštěvníci z našeho odkazu by pak byli automaticky přihlášení pod naším jménem, je zřejmé, že tento stav není ideální.

Příklad: ukázka session souboru
Uvedli jsme, že data samotná jsou ukládána v souborech na serveru. Níže je příklad, jak může obsah takového souboru vypadat.

název: sess_80fddut26lnhnhi8c5p5729ji6
obsah:
    user|a:2:{s:5:"login";s:7:"Alphard";s:4:"role";s:5:"admin";}
    pageViews|i:52;
    messages|a:1:{i:0;a:2:{i:0;s:18:"Článek byl uložen.";i:1;s:2:"ok";}}
    settings|a:1:{s:6:"design";s:4:"dark";}

Terminologie


V českých překladech najdete pro session různá pojmenování jako relace, sezení nebo dokonce seance. Tento článek se nesnaží překládat zavedené anglické termíny za každou cenu, takže preferuje pojem session. V menší míře se zde setkáte s pojmenováním relace, kterou je obecněji myšlena spíše dlouhodobější interakce klienta se serverem než konkrétní implementace session.
Podobně zde mluvíme o cookie a ne sušence a v úvodu byl alespoň několikrát uveden pojem http request.

Práce se session


Pokud není nastavený session.auto_start (ve výchozím stavu není), zahájíme session relaci funkcí session_start(). Kvůli nastavování cookie musíme tuto funkci volat před jakýmkoliv výstupem, typicky zcela na začátku souboru (až na výjimky, např. ob_gzhandler musí být registrován před inicializací session). V této chvíli již můžeme začít pracovat se superglobálním polem $_SESSION (dříve $HTTP_SESSION_VARS). Hodnotu do session přiřadíme běžným operátorem přiřazení $_SESSION['time'] = time(). Přečíst (s vypsáním) ji můžeme standardní konstrukcí echo $_SESSION['time']. Obecně lze říci, že se session pracujeme jako s běžným polem, můžeme používat in_array, isset, unset a podobně všechny funkce pro práci s poli, jak byly uvedeny v díle o polích. Jediný rozdíl při práci mezi session a běžnými poli je jejich superglobálnost, tj. jsou viditelé v libovolné funkci.

Pozn. Na rozdíl od nastavení cookie pomocí setcookie() se změna session projeví ještě na téže stránce (změněná cookie až po reloadu, protože obvykle neupravujeme přímo $_COOKIE).

Příklad: základní práce se session – čas strávený na stránce a další statistiky

session_start();

$firstVisit = false;
if (!isset($_SESSION['firstVisit']))
{
    $_SESSION['firstVisit'] = new \Datetime;
    $_SESSION['lastVisit'] = new \Datetime;
    $_SESSION['count'] = 0;
    $firstVisit = true;
}
$timeSum = (new \Datetime)->diff($_SESSION['firstVisit']);
$timeLast = (new \Datetime)->diff($_SESSION['lastVisit']);
$lastVisit = $_SESSION['lastVisit'];
$_SESSION['lastVisit'] = new \Datetime;
$_SESSION['count']++;

echo ($firstVisit ? 'Vítejte, jste u nás poprvé.' : 'Děkujeme, že se k nám vracíte.') . PHP_EOL;
echo 'Čas první návštěvy: ' . $_SESSION['firstVisit']->format('H:i:s') . PHP_EOL;
echo 'Celkový čas strávený na našich stránkách: '.  $timeSum->format('%H:%i:%s') . PHP_EOL;
echo 'Čas předchozí návštěvy: ' . $lastVisit->format('H:i:s') . PHP_EOL;
echo 'Čas od předchozí návštěvy: ' . $timeLast->format('%H:%i:%s') . PHP_EOL;
echo 'Počet návštěv: ' . $_SESSION['count'] . PHP_EOL;
Vyzkoušejte si tento příklad:

Pozn. Pokud jste v nějakém starším návodě viděli funkci session_register() a podobné, zapomeňte na ně, jsou DEPRECATED, tj. již se nepoužívají. Využíváme výhradně konstrukce pro práci s poli.

Tip: Snažte se při práci se session udržovat určité namespace konvence, alespoň prací s vícerozměrnými poli. Předejdete tím konfliktům v názvech při práci s globální proměnnou na různých místech aplikace. Např. pro informace o aktuálním uživateli si vyhraďme klíč user: $_SESSION['user']['login'].

Mazání
Někdy potřebujeme některé hodnoty ze session zneplatnit/smazat. Vzhledem k předchozí radě udržovat určitou namespace strukturu zdůrazňuji formulaci některé hodnoty smazat. Podíváme-li se do manuálu k funkci session_destroy(), vidíme následující ukázku kódu.

session_start();
$_SESSION = array();
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}
session_destroy();
Co se stane? Smažeme veškerá data uložená v session, expirujeme cookie ukládající session identifikátor a na závěr ještě deaktivujeme session samotné. Pro jejich opětovné použití bychom museli znovu volat session_start().
Tím si rozbijeme aplikaci, např. smažeme případnou oznamovací zprávu o správném odhlášení řešenou pomocí session (viz dále) a přijdeme i o ostatní data uložená v session. Neustále myslete na to, že $_SESSION je superglobální proměnná, na konkrétním místě vždy pracujeme pouze s její konkrétní částí (ve vícerozměrném poli) a nikdy ji nemažeme celou.

Jak řešit mazání lépe?
Použijeme unset() na konkrétní klíč pole a vždy smažeme pouze ta data, která chceme smazat.

unset($_SESSION['user']);
session_regenerate_id(true);
případně
$_SESSION['user'] = [];
session_regenerate_id(true);
Takhle by mohlo vypadat odhlášení. Mažeme pouze část dat uloženou pod klíčem user (buď včetně tohoto klíče, nebo ho zachováme, ale uložíme do něj prázdné pole). Po přihlášení/odhlášení a dalších akcích, při kterých chceme zabránit session fixation, voláme funkci session_regenerate_id() s parametrem true, která vygeneruje nový název session a smaže ten původní (nesmazaná data v session jsou zachována, jen budou uložena pod jiným název, starou session tedy nejde zneužít). Tímto bezpečně odhlásíme uživatele a zachováme ostatní data v session nesouvisející s přihlašováním uživatelů.

Praktické využití

Přihlášení


Nejtypičtější využití session je identifikace uživatele prostřednictvím přihlášení se ke svému uživatelskému účtu. Předpokládejme existenci modelu spravujícího uživatelské účty, který mj. obsahuje metodu login() přijímající uživatelské jméno a heslo, obecněji vhodný identifikátor (např. OpenId). Kromě metody login() mějme ještě metodu getIdentity(), která nám vrátí identitu uživatele, neboli nějaké údaje z jeho profilu.

Příklad: Ukázka modelu použitého v následujícím příkladě

class UserAuthenticator
{
  private $users = [
    'Tony' => ['id' => 21, 'name' => 'Tony', 'password' => 'heslo1', 'role' => ['user', 'admin']],
    'Nina' => ['id' => 41, 'name' => 'Nina', 'password' => 'kocka', 'role' => ['user']],
    'Lenka' => ['id' => 65, 'name' => 'Lenka', 'password' => 'heslo1', 'role' => ['user']],
  ];

  public function getIdentity($name)
  {
    $identity = $this->users[$name];
    unset($identity['password']);
    return $identity;
  }

  public function login($name, $password)
  {
    if (isset($this->users[$name]) && $this->users[$name]['password'] == $password)
    {
      return $this->getIdentity($name);
    }
    else
    {
      return false;
    }
  }
}
Nyní samotný princip přihlášení. Uživateli se zobrazí stránka s přihlašovacím formulářem, ve kterém zadá jméno s heslem a odešle formulář. Na serveru převezmeme přijaté údaje a prostřednictvím metody login() se modelu zeptáme, jestli zadané údaje souhlasí s nějakým uživatelským účtem. Pokud ano, požádáme metodu getIdentity(), aby nám vrátila informace o uživateli. Nyní víme, že je uživatel přihlášený, máme načtená jeho přístupová práva, jeho uživatelská nastavení apod. Cílem je pamatovat si tohoto uživatele až do jeho odhlášení.
Identitu uživatele si tedy uložíme do session a budeme ji využívat při dalších návštěvách.

Tip: V praxi bychom nikdy neukládali hesla v plain textu, ale hashovali bychom je a ukládali pouze jejich hashe.

Příklad: Přihlášení

<?php
include 'UserAuthenticator.php'; // třída uvedená v předchozí ukázce
session_start();

$user = new UserAuthenticator;
$basePath = 'http://' . $_SERVER['SERVER_NAME'].$_SERVER['PHP_SELF'];

// odhlášení
if (isset($_SESSION['user']['id']) && isset($_GET['logout']))
{
    $_SESSION['user'] = [];
    session_regenerate_id(); // ochrana před session fixation

    header('location:'.$basePath, TRUE, 303);
    exit;
}

// přihlášení
if (isset($_POST['username']) && isset($_POST['password']))
{
    if (($identity = $user->login($_POST['username'], $_POST['password'])) !== false)
    {
        $_SESSION['user'] = $identity;
        $_SESSION['user']['time_logged'] = new Datetime;
        session_regenerate_id();

        header ('location:'.$basePath, TRUE, 303);
        exit;
    }
    else
    {
        header ('location:'.$basePath.'?incorrect_login=1', TRUE, 303);
        exit;
    }
}


#  ####################### sablona ##########################################
echo '<meta charset="utf-8" />';

  // ověření, jestli je uživatel přihlášen
  if (isset($_SESSION['user']['name']))
  {
    echo "Přihlášen: ".htmlspecialchars($_SESSION['user']['name'], ENT_QUOTES).
            " <a href=\"$basePath?logout=1\">odhlásit</a></p>";
    if (in_array('admin', $_SESSION['user']['role']))
    {
      echo "<a href=\"$basePath?new\">Napsat článek</a></p>";
    }
  }
  else
  {
      echo '<div>';
      if (isset($_GET['incorrect_login']))
      {
          echo "<p>Zadali jste neplatné uživatelské jméno nebo heslo</p>\n";
      }
      ?>
      <form action="<?php echo $basePath; ?>" method="post">
        Jméno: <input name="username" type="text"><br>
        Heslo: <input name="password" type="password"><br>
        <input name="submit" type="submit" value="Přihlásit">
      </form>
      </div>
<?php
  }
?>

Flash messages


V této kapitole bude demonstrováno užití session pro krátkodobé uložení dat.
Pod pojmem flash messages rozumíme stručné stavové zprávy informující uživatele zpravidla o proběhlých akcích. Modelově si představte následující situaci. Programujeme administraci určitých položek. Z hromadného výpisu vedou odkazy na detail položky s možností její editace. Po editaci uživatele (jen v případě správného provedení) přesměrujeme zpět na hromadný výpis, přesto je však nutné informovat obsluhu o správném provedení akce. Koncepční řešení není úplně snadné, protože konkrétní podoba hlášky vzniká na stránce A, ale zobrazit se má na stránce B. A to jenom za určitých podmínek (po aktualizaci stránky nebo při sdílení odkazu hlášku znovu nezobrazujeme).
Vhodným řešením je využití session jako krátkodobé úložiště. Na stránce A si uložíme danou hlášku a na stránce B ji vypíšeme. Výhodou tohoto řešení je velká obecnost a upravitelnost, protože mezi stránkami A a B není žádná pevná vazba.

Příklad: Vnitřní implementace flash messages

class Message
{
  protected $storage = [];
  protected $currentKey;

  public function __construct(&$s)
  {
    if (!isset($s['messages']))
    {
      $s['messages'] = [];
    }
    $this->storage = &$s['messages'];
  }

  public function getKey()
  {
    return $this->currentKey;
  }

  public function addMessage($m, $type = null)
  {
    if (empty($this->currentKey))
    {
      $this->currentKey = uniqid();
    }
    return $this->storage[$this->currentKey][] = [new Datetime, $m, $type];
  }

  public function getMessages($key)
  {
    $return = isset($this->storage[$key]) ? $this->storage[$key] : [];
    unset($this->storage[$key]);
    return $return;
  }
}
Na úvod poznamenejme, že klíčem k jednotlivým zprávám je identifikátor přenášený v URL. Kdybychom tento identifikátor vynechali a spoléhali se pouze na session, mohlo by se stát, že při paralelní práci ve více panelech bude někdy oznamovací hláška zobrazena v nesprávném panelu. Asi vás napadne, že když už máme v URL identifikátor, nepotřebovali bychom vůbec session, ale stačila by nám práce se soubory nebo s databází. To je pravda, proto referenci na session předáváme třídě jenom v parametru a necháváme si volné ruce pro snadné použití jiného úložiště. Nicméně session jsou vhodným místem pro uložení informací tohoto charakteru.

Podívejme se detailněji na uvedený příklad. V konstruktoru předáváme pole $_SESSION, na které se vytvoří reference uložená do interní proměnné $storage. $_SESSION je sice superglobální proměnná a bylo by možné přistupovat k ní přímo bez předávání parametrem, ale naším cílem by vždy mělo být omezovat vazby a závislosti. Díky tomu můžeme snadno změnit úložiště např. na souborový systém.

Příklad: Práce se stavovými zprávami

session_start();

$message = new Message($_SESSION);
$basePath = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['PHP_SELF'];
$flashSlug = 'flash';
$flashKey = isset($_GET[$flashSlug]) ? $_GET[$flashSlug] : '';

if (isset($_GET['action']))
{
  // činnost scriptu
  $message->addMessage('Akce byla úspěšně provedena.', 'ok');
  $message->addMessage('Výsledky jsou uloženy.', 'info');
  header('location:'.$basePath."?$flashSlug=".$message->getKey(), TRUE, 303);
}

# ################### šablona ######################
foreach ($message->getMessages($flashKey) as $m)
{
  $class = !empty($m[2]) ? ' class="'.$m[2].'"' : '';
  echo "<p$class>".htmlspecialchars($m[1], ENT_QUOTES)."</p>";
}

echo '<a href="'.$basePath.'?action=1">Akce</a>';
Přidání zprávy zajišťuje metoda addMessage(), její funkce je zřejmá z kódu. Pro navrácení relevantních zpráv využíváme getMessages(). Zde bychom v praxi mohli ještě kontrolovat stáří zpráv, v příkladu to pro jednoduchost nedělám.

Nákupní košík


Ukládání věcí vložených do nákupního košíku pomocí session je výhodné zvláště u neregistrovaných/nepřihlášených uživatelů (u přihlášených obvykle požadujeme dlouhodobé uložení). Umožníme uživatelům ukládat si produkty pro případný pozdější nákup a když k nákupu nedojde, PHP staré session samo uklidí. V tomto případě kombinujeme výhody obou předchozích příkladů, jak identifikaci uživatele, tak krátkodobé úložiště.
Přímo do session pole si ukládáme identifikátory produktů s dalšími informacemi. Pokud klient nákup dokončí, zkopírujeme uložené položky ze session do databáze.
Implementace je přímočará, pro inspiraci doplňuji jen odkaz na Nákupní košík od Jakuba Vrány.

Pro některé činnosti spojené s ukládáním dat během relace je možné využít session i cookie, uveďme si stručně zásadní rozdíly mezi nimi.
Z pohledu bezpečnosti lze říci, že identifikátor session je sice přenášen v cookie, ale obsah samotný je uložený na serveru. Díky tomu máme (při správně nastaveném serveru) data zcela pod kontrolou. Nikdo je nemůže číst, ani modifikovat. Lze sem bezpečně ukládat uživatelská práva, aniž by si je mohl uživatel sám změnit, apod.
Naopak obsah cookies je ukládán v prohlížeči a při každém požadavku posílán na sever. Nad jeho daty nemáme žádnou kontrolu. Kdokoliv si je může analyzovat i modifikovat.
Z praktického pohledu se na cookie můžeme dívat jako na dlouhodobější úložiště, avšak s omezenou kapacitou. Někdy se může hodit, že cookie lze upravovat v přímo prohlížeči pomocí javascriptu (někdy se to naopak nehodí), je to jedna z metod sdílení dat mezi klientem a serverem.
Na session se v principu dívejme spíše jako na krátkodobé bezpečné úložiště (i když lze nastavit i opak). Typické využití je na ukládání aktuálních informací o přihlášeném uživateli nebo probíhající akci.

Nastavení


V této části budou uvedeny nejdůležitější konfigurační volby. Jde o výběr z manuálového přehledu konfigurace session. Detaily prosím hledejte tam.

Další funkce


Uveďme si několik významnějších funkcí, kterým jinak nebyla věnována větší pozornost.

Tímto výčet končí, vynechány byly prakticky jenom DEPRECATED funkce session_register() a session_unregister(). Handlerům se věnuje následující kapitola.
Pro práci se session vám stačí znát minimum funkcí a výchozí nastavení obvykle není třeba příliš měnit. Důležité je pochopit mechanismus a práci s poli.

Handlery a vlastní úložiště session


V úvodu jsme si řekli, že samotná data se defaultně ukládají do souborů na serveru. Adresář, kam se session soubory ukládají, je určen volbou session.save_path. PHP nám navíc umožňuje zcela převzít kontrolu nad prací se session přes vlastní implementaci SessionHandlerInterface.

Příklad: Vlastní implementace SessionHandleru

class MySessionHandler implements SessionHandlerInterface
{
    private $savePath;

    public function open($savePath, $sessionName)
    {
        $this->savePath = $savePath;
        if (!is_dir($this->savePath)) {
            mkdir($this->savePath, 0777);
        }

        return true;
    }

    public function close()
    {
        return true;
    }

    public function read($id)
    {
        return (string)@file_get_contents("$this->savePath/sess_$id");
    }

    public function write($id, $data)
    {
        return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
    }

    public function destroy($id)
    {
        $file = "$this->savePath/sess_$id";
        if (file_exists($file)) {
            unlink($file);
        }

        return true;
    }

    public function gc($maxlifetime)
    {
        foreach (glob("$this->savePath/sess_*") as $file) {
            if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
                unlink($file);
            }
        }

        return true;
    }
}
Podívejme se blíže, jaké metody interface obsahuje.

Zatímco v aplikaci je $_SESSION pole, zde již pracujeme pouze s řetězci. Složitější struktury jsou serializovány na jednoduchý řetězec – pro snadnější uložení.
Nastavení serializačního handleru je možné provést prostřednictvím session.serialize_handler.
Na závěr se zavolá metoda close(), může trochu uklízet, ale moc činností pro ni zřejmě nevymyslíme.

Pro údržbu session záznamů se používají poslední dvě metody vyjmenované v interface. destroy() smaže konkrétní session položku (smazání souboru, odstranění databázového záznamu) a gc(), tj. garbage collector, sloužící pro smazání všech starých záznamů. Na uklízení nezapomínejte, počet session souborů by bez uklízení mohl velmi rychle narůst do obrovských rozměrů a zpomalovat aplikaci. Na druhou stranu, příliš časté spouštění garbage collectoru si může vyžádat zbytečně velkou režii.

Příklad: použití vlastního handleru
Zkopírujte a uložte si výše uvedenou implementaci handleru a nastavte ji pro použití s prvním příkladem. Předpokladem je mít vytvořený adresář sessions_storage.

ini_set('session.save_path', realpath('sessions_storage'));
include 'MySessionHandler.php';
$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();
// tady pokračuje kód prvního příkladu
Nyní již znáte detailnější informace o implementaci session. Vyzkoušejte podobné činnosti jako v prvním příkladu, ale zároveň sledujte, jak se mění jednotlivé soubory.

Pozn. jak vidíte v manuálu, session_set_save_handler() lze volat i s jednotlivě vyjmenovanými funkcemi v parametrech.

Chceme-li jen detailněji upravit určitou vlastnost defaultního chování, můžeme zdědit SessionHandler a upravit jen některé jeho metody. Nemusíme psát celou implementaci sami. Ale musíme zachovat kompatibilitu se zbytkem třídy (tj. nemůžeme si implementovat read a write pro práci s databází, ale garbage collector nechat čistit soubory.
V dokumentaci u SessionHandleru je uveden pěkný příklad šifrovaného ukládání dat pomocí přepsání metod read() a write().

Příklad: Modifikace defaultního SessionHandleru

public function read($id)
{
    $data = parent::read($id);
    return mcrypt_decrypt(MCRYPT_3DES, $this->key, $data, MCRYPT_MODE_ECB);
}

public function write($id, $data)
{
    $data = mcrypt_encrypt(MCRYPT_3DES, $this->key, $data, MCRYPT_MODE_ECB);
    return parent::write($id, $data);
}
Všimněme si, jak je vytvářena instance handleru.
$handler = new EncryptedSessionHandler('mykey');
session_set_save_handler($handler, true);
Třídě je v konstruktoru předáván klíč, pomocí kterého má probíhat šifrování. Tímto způsobem můžeme předat např. odkaz na službu pro komunikaci s databází nebo libovolná další nastavení. Můžeme využít i set metody.
$handler = new DbSessionHandler();
$handler->setDbConnection($dbConnection);
session_set_save_handler($handler, true);

Bezpečnost - nejen session hijacking


V prvním příkladě jsme si ověřili, že session jsou (bez dodatečných úprav) identifikovány výhradně pomocí cookie posílané v http požadavku a že když změníme obsah této cookie, můžeme se vydávat za jiného uživatele. Tato vlastnost je příčinou mnoha zranitelnosti, našli byste je pod názvy session fixation, sessions hijacking. apod. Naši cookie můžeme změnit vždy, problémem je (alespoň by mělo být) získat tu cizí od uživatele, za kterého se chceme vydávat.
Pokud používáme nešifrovanou komunikaci se serverem, přenáší se naše cookies viditelně po celé síti (nejhorším případem jsou veřejné wifi sítě). Bude-li někdo odposlouchávat síťovou komunikaci, zachytí hlavičky naší komunikace se serverem, podstrčí do svého zařízení naše identifikační údaje a může se za nás libovolně vydávat. (Při změně hesla vždy vyžadujte zadání toho původního, byť je uživatel přihlášen.) Bránit se spolehlivě proti tomuto útoku je možné jen šifrovaným připojením.

Někdy se používají dodatečné ochrany jako kontrola IP adresy (jestli se nezměnila od posledního přihlášení). Problémem je, že útočník odposlouchávající na naší síti často komunikuje právě přes tuto síť, takže mu tato ochrana nepůsobí potíže. Naopak touto „ochranou“ značně rozzlobíme uživatele s mobilním připojením, kterým se mění IP adresa.
Další metody jako kontrola stejného prohlížeče a dodatečných údajů v $_SERVER je téměř směšná. Jestli nám někdo dokáže odposlechnout identifikátor v cookie, podvrhne i tohle.

Problém s odposlouchávání sítě je bohužel nutné zobecnit na jakoukoliv komunikaci. Nešifrujete-li data, lze zachytit i přímo hesla zadaná do přihlašovacích formulářů. Ukázka, jak lze síťovou komunikaci zachytávat např. s pomocí Wireshark. Takovéto útoky bohužel nevyžadují žádné velké umění nebo technické znalosti, jsou na (nebo možná i pod) úrovni Script kiddie.

V kapitole Nastavení byly uvedeny různé možnosti nastavování session a cookie. Zdůrazněme a doplňme nyní některé související s bezpečností.
V první řadě opět připomínám session.cookie_httponly, je nezbytné nepřenášet session identifikátor v URL, podobně též negativně nastavené session.use_trans_sid říká, že PHP nebude zahrnovat ani číst identifikátory v URL. Dále session.use_strict_mode nepřijme neinicializované session nebo session.cookie_secure posílá cookie výhradně přes zabezpečené připojení. session.entropy_file a session.entropy_length umožňují nastavit cestu k externímu zdroji dat s vyšší entropií (např. /dev/random), tj. znesnadnit uhádnutí jména session souboru, který bude vygenerován. Možnost náhodné shody můžeme omezit nastavením silné hashovací funkce pomocí session.hash_function.

Session Upload Progress


S pomocí session je v PHP implementován mechanismus, který umožňuje sledovat průběh uploadování souboru.

Ve výchozím stavu je tento mechanismus povolen volbou session.upload_progress.enabled. Klíč je sestaven z nastavení session.upload_progress.prefix a session.upload_progress.name. Frekvenci aktualizací stavu nastavujeme v session.upload_progress.freq, kde můžeme zadat hodnotu v bajtech nebo procentech, výchozí hodnota je 1 %. Aby se předešlo zbytečně častým aktualizacím, existuje volba session.upload_progress.min_freq s minimálním časem mezi dvěma aktualizacemi stavu, defaultně je nastavená na 1 s.
PHP bude automaticky aktualizovat stav uploadu do session, pokud se v přijímaném POSTu nachází proměnná pojmenovaná jako session.upload_progress.name.

Příklad: naznačení implementace
Do formuláře pro upload přidáme skryté pole identifikující upload.

<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
Z PHP manuálu si vypůjčím následující příklad obsahu session na serveru během uploadu.
$_SESSION["upload_progress_123"] = array(
 "start_time" => 1234567890,   // The request time
 "content_length" => 57343257, // POST content length
 "bytes_processed" => 453489,  // Amount of bytes received and processed
 "done" => false,              // true when the POST handler has finished, successfully or not
 "files" => array(
  0 => array(
   "field_name" => "file1",       // Name of the <input/> field
   // The following 3 elements equals those in $_FILES
   "name" => "foo.avi",
   "tmp_name" => "/tmp/phpxxxxxx",
   "error" => 0,
   "done" => true,                // True when the POST handler has finished handling this file
   "start_time" => 1234567890,    // When this file has started to be processed
   "bytes_processed" => 57343250, // Number of bytes received and processed for this file
  ),
  // An other file, not finished uploading, in the same request
  1 => array(
   "field_name" => "file2",
   "name" => "bar.avi",
   "tmp_name" => NULL,
   "error" => 0,
   "done" => false,
   "start_time" => 1234567899,
   "bytes_processed" => 54554,
  ),
 )
);
Jediná věc, kterou musíme nyní udělat, je vydělit dosud uploadovanou část celkovou velikostí.
session_start();
$key = ini_get("session.upload_progress.prefix") . '123';
if (!empty($_SESSION[$key])) {
    $current = $_SESSION[$key]["bytes_processed"];
    $total = $_SESSION[$key]["content_length"];
    echo $current < $total ? ceil($current / $total * 100) : 100;
}
else {
    echo 100;
}
Poslední úsek kódu je převzatý z Tracking Upload Progress with PHP and JavaScript, kde je uvedeno kompletní řešení včetně paralelních XHR požadavků (AJAX) a základního stylování, na které zde není místo.

Na závěr upozorňuji, že tato technika nemusí být vždy funkční. Problém nastává, když PHP nemůže sledovat upload a vidí ho až po dokončení, tj. v případech nastaveného bufferování (často na serverech Nginx) nebo když je PHP spouštěno přes FastCGI.

Tento mechanismus není jediná možnost, jak progress bar vytvořit. S použitím HTML5 ho lze řešit pomocí javascriptu výhradně na úrovni klienta (soubor samotný se uploaduje přes XHR, takže můžeme sledovat průběh), uploadery využívající flash to umí již dávno.

Pokročilejší metody práce se session


V předchozích kapitolách byly uvedeny velké možnosti nastavení session. Určitým problém je, že obvykle chceme s různými session proměnnými pracovat odděleně a někdy je i nastavovat odlišně. Proto různé frameworky vytváření OOP obálku nad superglobálním polem $_SESSION, která obsah session rozděluje do oddělených sekcí (již dříve jsem doporučoval tyto sekce alespoň simulovat vícerozměrným polem) a pro jednotlivé sekce poskytuje lepší možnosti nastavení. Každé sekci pak můžeme třeba nastavit různou životnost a zaručit zneplatnění po této životnosti.
Součástí frameworků/doplňků bývají hotové handlery pro ukládání dat v databázi nebo memcache, nemusíme tedy vše tvořit od začátku.

Vyzkoušejte si


Tato kapitola je hodně vysvětlující (ať již samotný princip nebo bezpečnostní aspekty). Praktických věcí je zde méně.
  1. Určitě si vyzkoušejte první příklad s body doporučenými k testování.
  2. Vyzkoušejte si příklad s přihlášením. Doplňte ho o formulář, který přihlášeným uživatelům umožní provést nějaké nastavení a to si pamatujte (a vypisujte) po dobu přihlášení.
  3. *Zájemcům o hlubší pochopení problematiky doporučuji vyzkoušet si některý z příkladů s vlastním handlerem pro práci se session. Uvedený příklad MySessionHandler pracující se soubory můžete přepsat pro spolupráci s databází.


Správcem webu Péhápko.cz je Joker, mail zavináč it-joker tečka cz. Informace o autorských právech a možnostech použití obsahu viz Autorská práva
Přihlášení