Trvalé přihlášení

Školení, která pořádám

Session proměnné v PHP, které se obvykle používají pro uložení informace o přihlášení uživatele, mají bohužel ve výchozím nastavení krátkou platnost (24 minut nebo do zavření prohlížeče). Tuto platnost můžeme prodloužit konfiguračními direktivami session.cookie_lifetime a session.gc_maxlifetime, ta ale nejde nastavit selektivně. Pro zajištění trvalého přihlášení je tedy možné uložit si informaci o přihlášení jinam (nejspíš do databáze) a uživateli nastavit samostatnou cookie s libovolnou platností.

Při návrhu databáze musíme počítat s tím, že jeden uživatel se může trvale přihlásit i víckrát (třeba z různých počítačů) a může chtít odhlásit jen jednu instanci. Data tedy uložíme do tabulky uzivatele_prihlaseni(id_uzivatel, token). Kód je potom poměrně jednoduchý:

<?php
// prvotní přihlášení
$token = md5(uniqid(mt_rand(), true));
setcookie("trvale_prihlaseni", "$_SESSION[id_uzivatel]:$token", strtotime("+1 month"));
mysql_query("
    INSERT INTO uzivatele_prihlaseni (id_uzivatel, token)
    VALUES ($_SESSION[id_uzivatel], '$token')
");

// automatické přihlášení
list($id_uzivatel, $token) = explode(":", $_COOKIE["trvale_prihlaseni"], 2);
mysql_result(mysql_query("
    SELECT COUNT(*)
    FROM uzivatele_prihlaseni
    WHERE id_uzivatel = " . intval($id_uzivatel) . "
    AND token = '" . mysql_real_escape_string($token) . "'
"), 0);

// odhlášení
setcookie("trvale_prihlaseni", "", 1);
list($id_uzivatel, $token) = explode(":", $_COOKIE["trvale_prihlaseni"], 2);
mysql_query("
    DELETE FROM uzivatele_prihlaseni
    WHERE id_uzivatel = " . intval($id_uzivatel) . "
    AND token = '" . mysql_real_escape_string($token) . "'
");
?>

Uživatele identifikujeme pomocí náhodného řetězce a do cookie pro jistotu ukládáme i jeho ID. Pokud máme tu možnost, je lepší cookie nastavovat jen pro protokol HTTPS. Uživateli je vhodné nabídnout i možnost zrušit všechna trvalá přihlášení pro případ, že se zapomene odhlásit na počítači, ke kterému už nemá přístup.

Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.

Jakub Vrána, Řešení problému, 14.12.2009, diskuse: 31 (nové: 0)

Diskuse

ikona tiso:

Tomu sa vraví presné trafenie témy, práve toto budem potrebovať. Pre seba som používal jednoduchšie riešenie, teraz vidím jeho slabiny.
P.S.: oprav si preklepy "nebo nebo" a "zapomene přihlásit" -> "zapomene odhlásit"

ikona Jakub Vrána OpenID:

Díky za upozornění, překlepy jsem opravil.

fos4:

Lze nejak resit prihlaseni po vice ucty v jednom prohlizeci ? Asi jen pomoci sess_id v url, nepletu se ?

ikona Jakub Vrána OpenID:

Nemusí to být zrovna sess_id, mohlo by to být třeba i user_id, ale osobně bych to neřešil. Když něco takového sám potřebuji, tak si prohlížeč spustím s druhým profilem.

ikona v6ak:

Přesně tak, proč zhoršovat zbytku uživatelů komfort a komplikovat si to session id nebo user id v URL (dobře, v Nette by to bylo snadné) a případnými bezpečnostními problémy (dobře, u user id by to tak nevadilo).

fos4:

Byla to spise recnicka otazka jak tento problem vyresit, aby prihlaseni pod vice ucty fungovalo v ruznych tabech. Je jasne ze cookie pouzit nejde takze jedine co zbyva je predavat token  v URL.
Bezpecnost v pripade predavani v URL pokulhava, ale i s cookie to neni moc slavne, vetsinou se zapomene na pouziti HTTP only a pak jsou dostupne napr. z javascriptu.

fos4:

Vlastne i zde by se hodilo pridat HTTP only, pro pripad uspesneho XSS.

ikona v6ak:

To není samo o sobě bezpečnostní chybou. Horší je zapomenout secure u SSL, to už bezpečnostní chyba je.

Tomas:

Pri uspesnom automatickom prihlaseni by to este chcelo predlzit platnost cookie, ale to je snad samozrejmost.

ikona Jakub Vrána OpenID:

Nejsem si jist. To, že trvalé přihlášení vydrží třeba jen měsíc, může být vlastnost, nikoliv chyba.

Tomáš Müller:

Pozor ovšem na to, že explicitně stanovená platnost cookie (tj. jiná než do zavření okna browseru) je závislá na klientském čase. Onehdá jsem si kvůli tomu docela naběhl (platnost cookie 14 dní, některým uživatelům ale nic nefungovalo...) a problém byl v tom, že uživatelé měli nastavený čas třeba i o dva roky mimo.

ikona Jakub Vrána OpenID:

Logicky by to přitom mělo fungovat tak, že si prohlížeč spočte relativní platnost podle hlavičky Date.

ikona Milan Kryl:

Je dobré pak uživatelům nabídnout i možnost odhlášení ze všech přihlášených instancí (například Gmail tohle umožňuje v kombinaci se zobrazením IP adresy u jednotlivých "tokenů").

ikona Jakub Vrána OpenID:

Ano, přesně na tohle upozorňuji i v článku: "Uživateli je vhodné nabídnout i možnost zrušit všechna trvalá přihlášení".

Michal:

Proč zapisovat informace o přihlášení i do databáze? Logicky stačí ukládat je do cookies (např. společně s hashem hesla a ID uživatele) a ověřovat je přímo proti uživatelskému účtu.

ikona Jakub Vrána OpenID:

Z bezpečnostního hlediska není vhodné do cookie ukládat tak střeženou informaci jako je heslo uživatele. S hashem hesla si moc nepomůžeme, protože pokud se tento hash dá použít pro přihlášení, tak má vlastně váhu hesla (útočníkovi stačí zjistit tento hash, aby se mohl kdykoliv přihlásit).

Proto se vedle hesla zavádí ještě token pro trvalé přihlášení. Pokud dojde k jeho kompromitaci, tak ho stačí smazat a heslo uživatele zůstane bez úhony.

tomáš:

A může tedy někdo ukrást mojí cookie s id a tokenem a pomocí toho se přihlásit? Vypadá to totiž jednoduší než práce se sessions. Tak mě zajímá, jestli to je i bezpečnejší. děkuju

Vojtěch Vondra:

Teoreticky může, musel by ale uhádnout token, což právě díky hashy času příhlášení není tak jednoduché.

Je vhodné zakomponovat do toho hashe nějaký veřejně neznámý údaj z uživatelova účtu pro dodatečné ověření.

I když uniqid(), které je jiné každou tisícinu vteřiny také stačí.

ikona Jakub Vrána OpenID:

Dodám, že samotný hash času by nestačil, protože čas se dá docela přesně uhodnout z platnosti cookie. Důležitá je nějaká náhodná hodnota.

tqs:

Souhlasím, že Vás způsob je bezpečnější.
Ale zeptám se jinak: povolení trvalého přihlášení je samo o sobě nebezpečné (uživatel povolí trvalé přihlášení a neuvědomí si, že stejný počítač používá ještě někdo jiný). Potom mi přijde další riziko v podobě ukládání hashe hesla přímo do cookie jako zanedbatelné. Nebo se mýlím a něco zásadního jsem přehlédl?

ikona Jakub Vrána OpenID:

Token se dá zneužít jednorázově, uživatel ho navíc může zneplatnit i z jiného přihlášení. Uložené heslo (nebo jeho hash, to vyjde na stejno) lze zneužít trvale.

ikona v6ak:

Je to celkově zvláštní řešení. Musíš navíc řešit například funkčnost přihlášení po změně hesla.

Navíc k přihlášení může být více cest - nejen jméno + heslo, ale třeba i OpenID. Pak to tu trošku kulhá.

Tomáš Fejfar:

Přihlášen/Nepřihlášen by mělo být oddělené od autorizačního procesu. Takže jestli se uživatel loguje OpenID, jeménem nebo třeba přes LiveID je úplně jedno IMO. Pseudokod:

<?php if(hesloSouhlasi())
  jmeno = ziskejIdUzivatele(jmeno, md5(heslo))
  prihlasUzivatele(jmeno);

if(
prostredniUkazkovyKodNahore())
  prihlasUzivatele(jmenoZCookie)
?>

ikona v6ak:

Až na toto:
* nelze změnit hash v průběhu (začal jsem o tom psát článek)
* kód je celkově zvláštní a možná by si zasloužil ještě jednou přečíst

Bendík:

Moc díky za kód. Velmi mi pomohl.

phebix:

Pěkný kód, akorát je tu malý problémek, a to pokud někomu vyprší trvalé přihlášení - cookies se po té době smažou ale co záznamy v databázi? Ty nic nesmaže a počet takovýchto záznamu bude v databázi narůstat.
Uvažoval jsem nad tím ukládat do databáze čas poslední použití. A pak jednou začas vymazat záznamy jejichž doba od posledního použití bude větší jak doba platnosti cookies.

Petr Kadeřábek:

Dobrý den,
Naučil jsem se nastavovat platnost session tím nejjednodušším způsobem, tedy - session_set_cookie_params(doba platnosti);
session_start();

tento kod uložím do samostatného souboru a poté ho vnořým do každého skriptu.

Nyní jsem chtěl, ale nabídnout uživateli volbu - zda chce zůstat přihlášen - či ne, ale nevím jak to udělat

do přihlašovacího formuláře dám checkbox (name login) - poté bych ověřil $_POST['login'] - pokud existuje nastavil bych platnost, když né nechám po zavření "zničit" relaci.

Ale řeším 2 problémy:
1) jak informovat ostatní skripty, jakou volbu uživatel zaškrtl - napadlo mě že vytvořím $_SESSION['login'] a tam vložím hodnotu podle toho, co uživatel zaškrtl
tuto hodnotu poté ověřím a podle toho nastavím platnost
Ale nevím jestli je to tak dobře? (1. otázka)

2) nevím jak to správně nakodovat, abych mohl mít před session_start() atd. jiný kod - tedy podmínku - if..else
(2. otázka)

Mohl by jste mi někdo poradit, nejlépe by bylo, kdyby jste napsali příklad, nebo samozřejmě jiné řešení, ale budu vděčný za každou pomoc

ikona Jakub Vrána OpenID:

Před session_start() může být libovolný kód, jen nesmí nic vypisovat.

Nastavení platnosti podle uživatele se dá udělat takhle:

<?php
session_start
();
session_write_close();
session_set_cookie_params(); // V závislosti na volbě uživatele.
session_start();
?>

David:

Nemali by byt tieto dva riadky obracene? Najskor zistit udaje z aktualnej cookie a pak tuto cookie smazat.
<?php
// odhlášení
setcookie("trvale_prihlaseni", "", 1);
list(
$id_uzivatel, $token) = explode(":", $_COOKIE["trvale_prihlaseni"], 2);
?>

ikona Jakub Vrána OpenID:

$_COOKIE se nastavuje na začátku stránky, volání setcookie() to neovlivňuje.

David:

ok diky, ale pre istotu som to dal v obratenom poradi, dufam ze to nicomu neuskodi :)

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.