Autorizácia v PHP s MySQL

23. října 2003

Vytvoriť bezpečnú autorizáciu, čiže prístup na stránky pod užívateľským menom a heslom, je neľahká úloha. Ako ošetriť neoprávnené prihlásenie pomocou PHP so sessions v kombinácii s databázou MySQL si prečítate v tomto článku.

Ako teoretický úvod poslúži článok Bezpečnost především – cross-site skripting a session-stealing, ktorý varuje pred možnými chybami pri písaní takéhoto skriptu. Vychádzajme z obvyklej situácie, kde máme užívateľov systému zapísaných napr. v tabuľke uzivatelia, ktorá obsahuje položky uid (jedinečné číslo užívateľa), meno (prihlasovacie meno) a položku heslo, kde budeme uchovávať heslo užívateľa samozrejme zahashované – v našom prípade algoritmom md5. SQL príkaz pre vytvorenie tabuľky uzivatelia bude nasledovný:

CREATE TABLE uzivatelia (
  uid int(10) unsigned NOT NULL auto_increment,
  meno varchar(12) NOT NULL,
  heslo varchar(32) NOT NULL,
  UNIQUE uid (uid)
);

Pre autorizačný skript budeme potrebovať ešte jednu tabuľku autorizacia, ktorá nám poslúži na evidenciu prihlásených užívateľov, konkrétne položky sess (session id), cas (čas prihlásenia, resp. čas posledného prístupu na server) a položka user_info do ktorej si zapíšeme údaje o IP adrese užívateľa pri prihlásení a počas celej doby prihlásenia budeme hodnotu tejto položky kontrolovať s aktuálnym stavom. Opäť uvádzam SQL skript pre vytvorenie tabuľky autorizacia:

CREATE TABLE autorizacia (
  aid int(10) unsigned NOT NULL auto_increment,
  sess varchar(32) NOT NULL,
  cas int(10) unsigned DEFAULT ‚0‘ NOT NULL,
  user_info varchar(32) NOT NULL,
  UNIQUE aid (aid)
);

Za jednou IP adresou sa môže nachádzať niekoľko rôznych počítačov, preto je potrebné skutočnú IP adresu zisťovať spôsobom popísaným v hore citovanom článku:

$IPadresa=$REMOTE_ADDR;
$IPadresa.=“@“.$HTTP_X_FORWARDED_FOR;
$IPadresa.=“@“.$HTTP_FORWARDED;
$IPadresa.=“@“.$HTTP_CLIENT_IP;
$IPadresa.=“@“.$X_HTTP_FORWARDED_FOR;

Čo, žiaľ, taktiež nerieši jedinečnú identifikáciu počítača, vo väčšine prípadoch bude nastavená len premenná $REMOTE_ADDR, ostatné premenné môžu, ale aj nemusia byť nastavené. Proxy posielajú tieto HTTP hlavičky, ale nastavením je možné ich zakázať. Ja využívam na identifikáciu IP adresy funkciu getVisitorIdentifier, ktorú nájdete na stránke identifier PHP functions by Marc Meurrens, čo je v podstate upravená vyššie uvedená identifikácia s rozlíšením rôznych prípadov nastavení (viac napovedia komentáre vo funkcii).

Kontrola IP adresy počas doby prihlásenia môže spôsobovať problémy dial-up užívateľom, u ktorých sa IP adresa mení dynamicky pri každom pripojení. Netreba sa však obávať toho, že prídeme o dial-up užívateľov, treba ich však na túto situáciu pri prihlasovaní upozorniť.

Jedinečná identifikácia užívateľa (resp. počítača) nie je možná, avšak k funkcii getVisitorIdentifier môžeme doplniť ako dodatočnú informáciu typ prehliadača, čím zvýšime presnosť identifikácie. Preddefinovaná premenná $_SERVER[‚HTTP_USER_AGENT‘] obsahuje reťazec identifikujúci prehliadač, napr. Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.1) Opera 5.12 [en].

Na ukážku fungovania autorizácie si vytvoríme nasledovné súbory:

  • index.php – hlavná stránka (cez túto stránku vkladáme obsah ďaľších stránok, cez require)
  • head.php – hlavička stránky
  • foot.php – päta stránky
  • settings.php – nastavenia
  • login.php – formulár pre prihlásenie
  • autorizacia.php – funkcia pre autorizáciu
  • dalsia1.php, dalsia2.php, dalsia3.php – súbory, ktoré sa budú vkladať ako obsah stránky podľa zvoleného menu

Súbor index.php obsahuje funkciu Autorizacia, ktorá pri úspešnom prihlásení vráti true, inak false a vloží formulár pre prihlásenie užívateľa. Na základe zvoleného menu vkladáme obsah stránok dalsia1.php, dalsia2.php, dalsia3.php. Výstup stránky posielame do bufferu, pretože ďalej v skripte voláme funkciu SetCookie, ktorú je potrebné volať ešte pred nejakým výstupom. Viac o posielaní výstupu do bufferu sa môžete dočítať v článku Ušetřete až 80 % datového přenosu.

<?php
ob_start ();
session_start ();
require „settings.php“;
require „autorizacia.php“;
require „head.php“;
if (Autorizacia () == false) {
  require „login.php“;
  require „foot.php“;
  exit ();
  }
switch ($_GET[„menu“]){
  case „dalsia2“:
  case „dalsia3“:
    require $_GET[„menu“].“.php“;
    break;
  default:
    require „dalsia1.php“;
    break;
  }
require „foot.php“;
ob_end_flush ();
?>

Súbor head.php obsahuje pripojenie k databáze a menu s odkazmi na ďalšie stránky <a href="index.php?menu=dalsia1<?php echo (SID?"&amp;".SID:"");?>">Dalsia 1</a>, teda ak je to potrebné, doplníme session id do odkazov (session.use_trans_sid = 0). Ak je to možné, použite perzistentné spojenie.

Súbor foot.php uzatvára HTML obsah stránky a spojenie s databázou. Súbor settins.php obsahuje nastavenia k autorizácii, význam je zrejmý z komentárov.

<?php
// debug mode
define („MK_DEBUG“, true);
// typ prihlasenia (0=select, 1=input)
define („AUTH_TYPE“, 1);
// timeout autoodhlasenia (v sekundach)
define („AUTH_TIMEOUT“, 60 * 60);
// ci bude spravcovske heslo platit pre vsetkych
define („AUTH_USEADMINPASSWD“, false);
// ak bude prazdna tabulka uzivatelia, automaticky sa vytvori user AUTH_ADMINUSERNAME, ktory bude povazovany za spravcu (uid=1)
define („AUTH_ADMINUSERNAME“, „[správca]“);
// heslo pre automaticky vytvoreneho spravcu (md5)
define („AUTH_ADMINPASSWD“, „21232f297a57a5a743894a0e4a801fc3“);
// platnost cookie (10000 dní)
define („AUTH_EXPIRECOOKIE“, 60*60*24*(10000));
?>

Súbor login.php obsahuje formulár pre prihlásenie užívateľa. Na základe konštanty AUTH_TYPE generujeme HTML formulár s input alebo option prvkami pre zadanie užívateľského mena. Ak tabuľka uzivatelia neobsahuje žiadny záznam, bude vytvorený užívateľ AUTH_ADMINUSERNAME s heslom AUTH_ADMINPASSWD. Premenná $username, získaná cez $username = $_POST["username"] ? $_POST["username"] : $_COOKIE["username"];, bude obsahovať hodnotu naposledy prihláseného užívateľa (zistené z cookies), alebo hodnotu aktuálne prihlasovaného užívateľa (napr. pri nesprávne zadanom hesle). V input prvku sa dosadí username ako hodnota, pri option prvku bude táto voľba s parametrom selected. JavaScriptom nastavíme kurzor do potrebného prvku.

<?php
$username = $_POST[„username“] ? $_POST[„username“] : $_COOKIE[„username“];
?>

<strong>Autorizácia užívateľa</strong>
<form method=“post“ action=“index.php“ name=“formular“>
<input type=“hidden“ name=“PHPSESSID“ value=“<?php echo session_id ()?>“ />
Užívateľ : <?php echo HTMLlogin ($username)?><br />
Heslo : <input type=“password“ name=“passwd“ />
<input type=“submit“ value=“Prihlásenie“ />
</form>
<script type=“text/javascript“>
function Focuzz () {
  <?php
  if (empty($username))
    echo „document.formular.username.focus ()\n“;
  else
    echo „document.formular.passwd.focus ()\n“;
  ?>

  }
window.onload = Focuzz
</script>

Funkcia generujúca formulárový prvok pre zadanie užívateľského mena, podľa konštanty AUTH_TYPE:

<?php
function HTMLlogin ($username) {
  $query = „SELECT * FROM uzivatelia ORDER BY meno“;
  $result = mysql_query ($query) or die();
  // ak neexistuju uzivatelia, vytvor spravcu systemu s heslom
  if (mysql_num_rows ($result) == 0) {
    $result = mysql_query („INSERT INTO uzivatelia (meno, heslo) VALUES (‚“.AUTH_ADMINUSERNAME.“‚, ‚“.AUTH_ADMINPASSWD.“‚)“);
    $result = mysql_query ($query) or die();
    }
  if (AUTH_TYPE == 0){
    $s = „<select name=’username‘>\n“;
    while ($row = mysql_fetch_object ($result)) {
      if ($username == $row->meno)
        $s .= „<option value=’$row->meno‘ selected style=’background-color:#ffffff‘>$row->meno</option>\n“;
      else
        $s .= „<option value=’$row->meno‘ style=’background-color:#e0e0ff‘>$row->meno</option>\n“;
      }
    $s .= „</select>\n“;
    }
  else if (AUTH_TYPE == 1) {
    $s = „<input type=’text‘ name=’username‘ value=’$username‘ />\n“;
    }
  return $s;
  }
?>

Súbor autorizacia.php obsahuje funkcie pre spracovanie prihlasovacieho formulára a následne kontrolu session id a zahashovanú hodnotu IP adresy s typom browsera. V úvode funkcie kontrolujeme, či nebolo zvolené odhlásenie daného užívateľa, ak áno, funkcia Logout sa postará o vymazanie záznamu s týmto session id z tabuľky autorizacia a odstráni session premenné (session_destroy by spôsobilo vymazanie session a session_id by bolo prázdne, preto si toto session id poneháme aj pre ďalšie prihlásenie, nič tým nepokazíme, len odstránime jeho premenné).

function Autorizacia () {
  if ($_GET[„menu“] == „logout“){
    Logout ();
    return false;
  }
  …
  …
function Logout () {
  // zmaz zaznam so session_id
  $result = mysql_query („DELETE FROM autorizacia WHERE sess='“.session_id ().“‚“) or die();
  // odstran session premenne
  reset ($_SESSION);
  while (list ($key, ) = each ($_SESSION))
    unset($_SESSION[$key]);
  }

V ďalšom kroku vyhodnotíme podmienku isset($_POST["username"]) && isset($_POST["passwd"]) && empty($_SESSION["uid"]), ktorá nám pri splnení hovorí o tom, že boli vyplnené vstupné hodnoty a užívateľ sa práve prihlasuje (ešte nie je prihlásený), čo ošetruje situáciu, ak by bol ihneď po prihlásení vyvolaný refresh stránky. Zadané heslo zahashujeme funkciou md5 $_POST["passwd"] = md5 ($_POST["passwd"]);. Ak bude nastavená konštanta AUTH_USEADMINPASSWD, správcovské heslo (záznam s uid=1) bude platné pre všekých ostatných užívateľov. Získanie správcovského hesla:

if (AUTH_USEADMINPASSWD) {
  // zisti spravcovske heslo
  $result = mysql_query („SELECT heslo FROM uzivatelia WHERE uid=’1′“) or die();
  $row = mysql_fetch_object($result);
  $admin_passwd = $row->heslo;
  }

Teraz sa pokúsime z tabuľky uzivatelia vybrať záznam so zadaným menom a heslom. Opäť bereme do úvahy aj nastavenie konštanty AUTH_USEADMINPASSWD:

// vyber uzivatela „username“ s heslom „passwd“
$result = mysql_query („SELECT * FROM uzivatelia WHERE meno='“.$_POST[„username“].“‚ AND heslo='“.$_POST[„passwd“].“‚“) or die();
if (AUTH_USEADMINPASSWD && !mysql_num_rows ($result)) {
  // zaznam neexistuje, kontroluj spravcovske heslo
  if ($admin_passwd == $_POST[„passwd“]) {
    // heslo je OK – vyber uzivatela „username“
    $result = mysql_query („SELECT * FROM uzivatelia WHERE meno='“.$_POST[„username“].“‚“) or die();
    return AuthInsert ($result);
    }
  }
return AuthInsert ($result);

Funkcia AuthInsert sa nám pri nájdení tohoto záznamu postará o pridanie užívateľa do tabuľky autorizacia, kde si pri vložení záznamu zapíšeme session id, čas prihlásenia a zahashované informácie o IP adrese a type prehliadača – funkcia GetUserInfo. Funkciu function getVisitorIdentifier som pridal z už spomínanej stránky Marca Meurrensa. Z tabuľky autorizacia ešte vymažeme záznamy, ktorým vypršal čas AUTH_TIMEOUT od poslednej akcie so serverom. Cez SetCookie nastavíme premennú $username na hodnotu práve prihláseného užívateľa, ktorú využívame v súbore login.php. Do superglobálnej premennej $_SESSION vložíme hodnoty, ktoré môžeme načítať napr. z tabuľky užívatelia a využívať ich počas doby prihlásenia. Povinná je hodnota uid, ktorú využívame v spomínanej podmienke funkcie Autorizacia isset($_POST["username"]) && isset($_POST["passwd"]) && empty($_SESSION["uid"]). Pri úspešnom pridaní záznamu do tabuľky autorizacia vráti funkcia AuthInsert hodnotu true, inak false – neúspešné prihlásenie. Funkcia HTMLlogout v dobe prihláseného užívateľa zobrazí odkaz n odhlásenie užívateľa (menu=logout).

function AuthInsert ($result) {
  if (mysql_num_rows($result)) {
    $row = mysql_fetch_object ($result);
    // pridaj session_id a cas prihlasenia do tabulky autorizacia
    mysql_query („INSERT INTO autorizacia (sess, cas, user_info) VALUES (‚“.session_id ().“‚, ‚“.time().“‚, ‚“.GetUserInfo().“‚)“) or die();
    // vymaz stare zaznamy
    mysql_query („DELETE FROM autorizacia WHERE cas < „.(time() – AUTH_TIMEOUT)) or die();
    setcookie(„username“, $row->meno, time() + AUTH_EXPIRECOOKIE);
    $_SESSION[„uid“] = $row->uid;
    $_SESSION[„username“] = $row->meno;
    $_SESSION[„ip_full“] = getVisitorIdentifier();
    $_SESSION[„browser“] = $_SERVER[‚HTTP_USER_AGENT‘];
    return HTMLlogout ();
    }
  return false;
  }
function GetUserInfo () {
  return md5(getVisitorIdentifier().$_SERVER[‚HTTP_USER_AGENT‘]);
  }
function HTMLlogout () {
  if ($_GET[„menu“] != „logout“)
    echo „<a href=’index.php?menu=logout“.(SID?“&amp;“.SID:““).“‚>Odhlásenie</a>“;
  return true;
  }

Ak je užívateľ prihlásený, je potrebné prekontrolovať, či sa záznam s daným session id a zahashovanou IP adresou s typom browsera nachádza v záznamoch tabuľky autorizacia. Pri existencii takéhoto záznamu prekontrolujeme, či nevypršal čas AUTH_TIMEOUT a nastavíme novú hodnotu času prístupu na server. Ak záznam neexistuje, alebo vypršal čas nečinnosti počas doby prihlásenia vráti nám funkcia Autorizacia hodnotu false (vymažú sa tiež ostatné záznamy s vypršaným časom a odstráni sa aktuálne session), inak vráti true.

// nebol zadany uzivatel ani heslo
// existuje zaznam s takymto session_id ?
$result = mysql_query („SELECT * FROM autorizacia WHERE sess='“.session_id ().“‚ AND user_info='“.GetUserInfo().“‚“) or die();
// ano existuje
if ( mysql_num_rows ($result) ) {
  $row = mysql_fetch_object ($result);
  // nevyprsal timeout ?
  if (time() – $row->cas < AUTH_TIMEOUT) {
    // update casu
    mysql_query („UPDATE autorizacia SET cas='“.time().“‚ WHERE aid='“.$row->aid.“‚“) or die();
    return HTMLlogout ();
    }
  // ano vyprsal
  else {
    mysql_query („DELETE FROM autorizacia WHERE cas < „.(time() – AUTH_TIMEOUT)) or die();
    session_destroy ();
    }
  }
return false;

Najdôležitejším bezpečnostným prvkom autorizácie bude protokol https. Korektne napísaný skript využívajúci https musí obsahovať všetky odkazy s absolútnou cestou, k tomu si medzi konštanty (settings.php) vložíme konštantu definujúcu absolútnu cestu define ("HTTPS_PATH", "https://localhost/autorizacia/"); k súborom, ktoré si želáme mať zabezpečené. V downloade zdrojových kódov nájdete tieto odkazy už upravené.

Ukladanie sessions na serveroch obvykle ostáva v prednastavenom adresári /tmp, avšak k zvýšeniu bezpečnosti je vhodné si session ukladať do nami zvoleného adresára, a to jednoduchým pridaním funkcie session_save_path("session/autorizacia/") ešte pred volaním session_start().

Okrem súborov môžeme session ukladať aj do databázy, čo pri správnom nastavení práv k databáze posunie úroveň bezpečnosti zas o niečo vyššie. Na ukladanie sessions do databázy je potrebná funkcia session_set_save_handler plus ďalšie nami definované funkcie k práci so sessions. Tieto funkcie môžeme nájsť na internete, napr. na stránke <!– ROZMERY IMG TREBA UPRAVIT PRI ZMENE KONSTANT V ImgRndStr.php –>
<img src=“imgrndstr.php<?php echo (SID?“?“.SID:““)?>“ width=“27″ height=“21″> <input type=“text“ name=“str“ size=“3″ />

Súbor ImgRndStr.php vytvára obrázok s náhodným reťazcom a taktiež tento reťazec zapíše do session. Po zadaní mena a hesla skontrolujeme, či je zadaný a session reťazec zhodný. Doplnok do funkcie Autorizacia (tučné písmo):

function Autorizacia () {
  if ($_GET[„menu“] == „logout“){
    Logout ();
    return false;
    }
  if (isset($_POST[„username“]) && isset($_POST[„passwd“]) && empty($_SESSION[„uid“])) {
    if($_POST[„str“]!=$_SESSION[„str“])
      return false;
    unset($_SESSION[„str“]);


Aby bolo aj pre počítač náročné rozpoznať, aký znak sa v obrázku nachádza, zvolil som pri generovaní reťazca metódu náhodného vyposúvania znakov v horizontálnom aj vertikálnom smere, samozrejme v hraniciach rozpoznania človekom. Pre zobrazenie znakov využívam základný font 5 (možné použiť 1 až 5, pričom väčšie číslo udáva väčší font), ktorý je súčasťou grafickej knižnice PHP (ak nie je zakompilovaná ako súčasť PHP treba v php.ini navoliť: extension=php_gd(2).dll (.so pre linux), alebo v úvode skriptu načítať cez funkciu dl). Keďže sa jedná o neproporcionálne písmo, v úvode si zistíme výšku a šírku znaku. Ďalej vytvoríme obrázok s rozmermi podľa počtu znakov a rozsahu horizontálnej premennej, ktorá určuje ako veľmi budú znaky „skákať“ v horizontálnom smere. Niektoré konštanty (farby, font, dĺžku reťazca, typ náhodných znakov a horizontálnu premennú) je možné jednoducho zmeniť na začiatku skriptu ImgRndStr.php. Znak nula, veľké a malé písmeno O sa v generovanom reťazci nevyskytuje (nahradené cez funkciu strtr), pretože sú navzájom dosť podobné. Náhodný posun vo vertikálnom smere je v rozsahu jedného pixelu. Taktiež je možné ako podklad vložiť obrázok, ktorý by ešte viac skomplikoval dekódovanie reťazca z obrázku.

Funkciu ImageColorAllocate som rozšíril o zadávanie farieb typu #rrggbb, ktorej názov je ImageColorAllocateHex.

function ImageColorAllocateHex ($im, $rgb) {
  $rgb = eregi_replace(„#“,““,$rgb);
  $r = hexdec (substr ($rgb,0,2));
  $g = hexdec (substr ($rgb,2,2));
  $b = hexdec (substr ($rgb,4,2));
  return ImageColorAllocate ($im, $r, $g, $b);
  }

A pre úplnosť kód súboru imgrndstr.php (pri ukladaní sessions do databázy vložiť pred session_start riadok require "session_mysql.php"; – ak preferujete túto možnosť, je vhodné upraviť skript tak, aby sme sa k databáze pripájali len raz):

<?php
###########################
// require „session_mysql.php“;
###########################

session_start ();
###############################
# konstanty
###############################

$bgcolor = „#cccccc“;
$color = „#000000“;
$font = 5;
$length = 3;
// 1 = (0..9)
// 2 = (0..9, A..Z)
// 3 = (0..9, A..Z, a..z)
$type = 2;
// kolko budu znaky „skakat“ v horizontalnom smere
$y = 6;
###############################
# samotny skript – generator
###############################

header („Content-type: image/png“);
// inicializacia nahodneho generatora (od verzie PHP 4.2.0 nepotrebna)
srand ((double) microtime() * 1000000);
$w = ImageFontWidth ($font);
$h = ImageFontHeight ($font);
$s=““;
for($i=0 ; $i<$length ; $i++)
  switch (rand() % $type) {
    case 0: $s.=chr (rand(ord(‚0‘),ord(‚9‘))); break;
    case 1: $s.=chr (rand(ord(‚A‘),ord(‚Z‘))); break;
    case 2: $s.=chr (rand(ord(‚a‘),ord(‚z‘))); break;
    }
$s = strtr ($s,“0Oo“,“1Aa“);
$im = ImageCreate ($w*$length, $h+$y) or die („GD error !“);
$bgcolor = ImageColorAllocateHex ($im,$bgcolor);
$color = ImageColorAllocateHex ($im,$color);
$m = rand(1,2);
for($i=0 ; $i<$length ; $i++) {
  if ($i)
    $m = $m>1 ? 0 : rand(-1,0);
  ImageChar ($im, $font, $w*$i+$m, rand() % $y, $s[$i], $color);
  }
ImagePng ($im);
$_SESSION[„str“] = $s;

Pre maximálnu bezpečnosť je možné naviac generovať dodatočné heslo a to potom zasielať užívateľom na mail, alebo formou SMS, ale pre bežné prihlasovanie myslím takýto spôsob postačí.

Súbory, potrebné k rozbehaniu autorizácie, sú vám k dispozícii.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Předchozí článek hypoexpres.cz
Štítky: Články

Mohlo by vás také zajímat

Nejnovější

4 komentářů

  1. mpca

    Bře 3, 2010 v 12:58

    Zdravíčko, túto autorizáciu mám nahodenú na localhoste, prihlásenie a odhlásenie funguje ok,,,
    Lenže stále mi vypisuje chybu Warning: gethostbyaddr() [function.gethostbyaddr]: Address is not a valid IPv4 or IPv6 address in /var/www/odvozb/getVisitorIdentifier.php on line 93 a neviem si dať rady ako to opraviť. Poradí niekto ako nato?

    Odpovědět
  2. mm

    Bře 15, 2010 v 18:51

    zdravim, urobil som podla tohot navodu stranku, normalne prihlasi , ale neviem ako mam urobi druhu stranku, napr. mam uvod, forum, pravidla, a som prihlaseny na uvode a chcem prepnut na pravidla . Som to urobil ako na dalsia 1 …2…3 ale ked prepnem na pravidla tak mi zobrazi iba prazdnu stranku, prosim poradte mi ako to mam rozchodit

    Odpovědět
  3. Anonym

    Lis 21, 2010 v 13:57

    /var/www/odvozb/getVisitorIdentifier.php on line 93

    treba opraviť
    93 riadok >> return ( gethostbyaddr($_SERVER[‚REMOTE_ADDR‘]) ) ;

    Odpovědět
  4. Aaa

    Říj 5, 2011 v 23:17

    V PHP 5.3.6. sa zobrazuju chyba s undefined index username a menu, nefunguje echo na HTTPS_PATH. mozno by to trebalo cele zrevidovat.

    Odpovědět

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *