Přidej si svou IT školu do profilu a najdi spolužáky zde na síti :)

5. díl - NERS - Registrace uživatelů v PHP

PHP Databáze pro začátečníky NERS - Registrace uživatelů v PHP American English version English version

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulém dílu našeho seriálu tutoriálů o databázích v PHP pro úplné začátečníky jsme si připravili projekt NERS - NEobjektový Redakční Systém v PHP a rozpracovali jsme skript registrace.php.

Nyní nad HTML blok vložíme blok s PHP obsluhou:

<?php
session_start();
require('Db.php');
Db::connect('127.0.0.1', 'ners_db', 'root', '');

if ($_POST)
{
        if ($_POST['rok'] != date('Y'))
                $zprava = 'Chybně vyplněný antispam.';
        else if ($_POST['heslo'] != $_POST['heslo_znovu'])
                $zprava = 'Hesla nesouhlasí';
        else
        {
                $existuje = Db::querySingle('
                        SELECT COUNT(*)
                        FROM uzivatele
                        WHERE jmeno=?
                        LIMIT 1
                ', $_POST['jmeno']);
                if ($existuje)
                        $zprava = 'Uživatel s touto přezdívkou je již v databázi obsažen.';
                else
                {
                        Db::query('
                                INSERT INTO uzivatele (jmeno, heslo)
                                VALUES (?, SHA1(?))
                        ', $_POST['jmeno'], $_POST['heslo'] . "t&#ssdf54gh");
                        $_SESSION['uzivatel_id'] = Db::getLastId();
                        $_SESSION['uzivatel_jmeno'] = $_POST['jmeno'];
                        $_SESSION['uzivatel_admin'] = 0;
                        header('Location: administrace.php');
                        exit();
                }
        }
}
?>

Jako první voláme session_start(). Funkce nám umožní používat tzv. session (česky relace nebo někdy i sezení), která si pamatuje data uživatele, se kterým komunikujeme. Tento řádek musí být na úplném začátku PHP souboru (ne bloku, opravdu souboru), ve kterém uživatelskou relaci používáme. Před session_start() se nesmí nalézat žádné HTML, ani prázdné řádky, ani mezery, jinak nebude fungovat. To samé platí pro funkci header().

Dále načteme Db wrapper a připojíme se k databázi. Údaje jsou pro localhost, vy již víte, že na produkci vám je sdělí webhosting.

Další kód je obsluha HTML formuláře. Pokud zadaný rok nesouhlasí s aktuálním, uložíme si chybovou zprávu. Jinak pokračujeme dále a stejným způsobem ověříme shodu obou zadaných hesel. Dostáváme se k samotné registraci.

Nejprve necháme databázi spočítat kolik je v ní uživatelů se zadaným jménem. Slouží k tomu SQL klauzule SELECT COUNT(*). Hvězdička označuje, že do výpočtu zahrnujeme všechny sloupce. Abychom databázi zbytečně zatěžovali, dáme na počet položek ještě LIMIT 1. Stačí nám, když zjistíme, že je v databázi nějaký uživatel s tímto jménem a dále se již databáze snažit nemusí. Dotaz zavoláme funkcí Db::querySingle(). Ta se používá v případě, když z databáze čteme pouze jednu hodnotu jednoho řádku. My zde čteme pouze jedno číslo - počet uživatelů s tímto jménem. Pokud jsme někoho našli, jméno je již obsazené a uložíme si chybovou hlášku.

SQL kód pro vložení uživatele do databáze je velmi podobný tomu z minulých lekcí. O něco složitější je zde ukládání hesla. Nejprve heslo uživatele vylepšíme a to přidáním tzv. soli (salt). To je úplně náhodný řetězec znaků, který se k heslu připojí kvůli bezpečnosti, nějaký si vymyslete. Uživatelé totiž rádi zadávají např. "heslo", "password", "123456" a podobně. Takto zadaná hesla jsou velmi nebezpečná a lze je jednoduše odhadnout. Po "osolení" z nich vznikne: "heslot&#ssdf54gh", "passwordt&#ssdf54gh­", "123456t&#ssd­f54gh", což prakticky znemožní zpětné uhodnutí onoho hesla. Osolené heslo nevložíme do databáze přímo a to proto, že databázi nám může někdo ukradnout a potom by viděl všechna hesla všech uživatelů.

Heslo uložíme pomocí SQL funkce SHA1(). To je tzv. hashovací funkce, která z daného hesla vypočítá otisk. Nejbezpečnější způsob uchování hesla v databázi je totiž heslo vůbec neuchovat, ale uchovat si pouze jeho otisk. Otisk je dlouhý řetězec zdánlivě nesouvisejících znaků a číslic a nelze z něj původní heslo zjistit. Hashovací funkce je tedy pouze jednosměrná. Když budeme chtít zjistit, zda uživatel zadal správné heslo, jednoduše ze zadaného hesla vypočítáme otisk stejným způsobem a tento otisk porovnáme s uloženým otiskem v databázi. To vše uvidíte až budeme programovat přihlašování.

Po vložení uživatele do databáze si vytvoříme uživatelskou relaci. V PHP je podobně jako $_GET nebo $_POST také superglobální pole $_SESSION. Do tohoto pole si můžeme ukládat data, které souvisí s tím uživatelem, se kterým právě komunikujeme. Relace vyprší ve výchozím nastavení PHP za 24 minut nečinnosti nebo po zavření prohlížeče. Jakmile ji jednou vytvoříme v nějakém skriptu, zůstanou nám její data přístupná i pro ostatní PHP skripty v aplikaci. My zde uživatele po registraci rovnou i přihlásíme a to tak, že do relace uložíme jeho id, jméno a zda je administrátor. Id (primární klíč) záznamu naposledy vloženého do databáze získáme pomocí funkce Db::getLastId(). Nakonec se přesměrujeme funkcí header() na stránku administrace.php a skript zastavíme. Pokud se něco nepodařilo, skript bude pokračovat a vypíše chybovou hlášku.

Zkusme si udělat nějakou chybu:

Registrace uživatele do systému v PHP

A nyní se úspěšně zaregistrujte. Skript vás přenese na stránku administrace.php, která ještě neexistuje. Podívejte se v phpMyAdmin do tabulky uzivatele (otevřete tabulku a vyberte z menu nahoře Projít). Vidíme zde nově vloženého uživatele a otisk jeho hesla:

Uložení hesla do MySQL databáze v PHP

Jako heslo jsem zadal "admin", výše vidíte otisk, co vrátila funkce SHA1().

Administrace

Vytvořme si nyní skript administrace.php. Bude to takový rozcestník, na který aplikace uživatele přenese po zaregistrování nebo po přihlášení. Jeho HMTL část bude následující:

<!DOCTYPE html>
<html lang="cs-cz">
<head>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="styl.css" type="text/css" />
        <title>Administrace</title>
</head>

<body>
<article>
        <div id="centrovac">
                <header>
                        <h1>Administrace</h1>
                </header>
                <section>
                        <p>Vítejte v administraci, jste přihlášeni jako <?= htmlspecialchars($_SESSION['uzivatel_jmeno']) ?></p>
                        <?php
                                if (!$_SESSION['uzivatel_admin'])
                                        echo('Nemáte administrátorská oprávnění, požádejte administrátora webu, aby vám je přidělil.');
                        ?>
                        <h2><a href="editor.php">Editor článků</a></h2>
                        <h2><a href="clanky.php">Seznam článků</a></h2>
                        <h2><a href="administrace.php?odhlasit">Odhlásit</a></h2>
                </section>
                <div class="cistic"></div>
        </div>
</article>
</body>
</html>
Kód obsahuje pouze 3 zajímavé věci
Vypisujeme zde jméno přihlášeného uživatele z relace (session).
Pokud není přihlášený uživatel administrátor, informujeme ho o tom.
Pro odhlášení uživatele odkážeme na skript administrace.php s GET parametrem odhlasit.
Nad kód vložíme jednoduchý PHP blok
<?php
session_start();
if (!isset($_SESSION['uzivatel_id']))
{
        header('Location: prihlaseni.php');
        exit();
}

if (isset($_GET['odhlasit']))
{
        session_destroy();
        header('Location: prihlaseni.php');
        exit();
}
?>

Výsledek:

Administrace článků v PHP

Pokud session neexistuje, přesměrujeme na přihlašovací stránku a zastavíme skript. Pokud je zadaný parametr odhlasit, zničíme session pomocí PHP funkce session_destroy() a opět přesměrujeme na přihlášení.

Příště budeme pokračovat.


 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
20 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity (1)

 

 

Komentáře
Zobrazit starší komentáře (77)

Avatar
David Čápka
Tým ITnetwork
Avatar
David Čápka:19. ledna 17:27

Díky, ono MySQL měnilo výchozí chování. Mám to v plánu upravit, v zakládacích skriptech by měla být výchozí hodnota.

Odpovědět  +1 19. ledna 17:27
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Rudolph Harsher - Nitrate:11. července 4:03

Dobrý den mám problém s tímto scriptem vždy když ho dám na web nezobrazí stranka a pouze se mi vypíše chyba Tato stránka nefunguje HTTP ERROR 500. Podle všeho by ale web by měl podporovat plně php. Nějak se s tím už delší dobu trápím protože bych rád dal dohromady redakční systém pro muj portál. http://obscurefreaks.tk/

 
Odpovědět 11. července 4:03
Avatar
Peter Schoeller:4. listopadu 21:58

Zdravim komunitu.
Dnes som sa vratil k tomuto skriptu a z dovodu ze vraj SHA1 uz nie je bezpecny som skusil opravit hash. Chcem sa opytat na vas nazor, ci to je bezpecne.
Povodna cast:

Db::query('
       INSERT INTO uzivatele (jmeno, heslo)
       VALUES (?, SHA1(?))
       ', $_POST['jmeno'], $_POST['heslo'] . "t&#ssdf54gh");

a moja upravena verzia: (mam tam viac parametrov na vlozenie, ale primarna otazka znie na bezpecnost hesla)

Db::query('
       INSERT INTO members (login, vorname, nachname, email, heslo, date_reg)
       VALUES (?, ?, ?, ?, ?, NOW())
        ', $_POST['login'], str_replace(' ','-', $_POST['vorname']), str_replace(' ','-', $_POST['nachname']), $_POST['email'], password_hash($_POST['heslo'], PASSWORD_BCRYPT));

Potom pri naslednom prihlasovani je tato cast:

$uzivatel = Db::queryOne('
     SELECT id, login, admin, heslo
     FROM members
     WHERE login=?
        ', $_POST['login']);
        if (!password_verify($_POST['heslo'], $uzivatel['heslo'])) {
        // chybove hlasenie
        } else {
        // pokracovanie

Dakujem za vase nazory.

Editováno 4. listopadu 22:00
 
Odpovědět 4. listopadu 21:58
Avatar
Marek Z.
Redaktor
Avatar
Odpovídá na Peter Schoeller
Marek Z.:4. listopadu 23:28

Ahoj,

toto je jedna z nejideálnějších možností, ještě by to chtělo nastavit cost parametr minimálně na 14.

Odpovědět  +1 4. listopadu 23:28
Chybami se člověk učí, běžte se učit jinam!
Avatar
Odpovídá na Marek Z.
Peter Schoeller:4. listopadu 23:48

Ano, mas pravdu
Aj na to som pozeral, ale nakoniec som od toho upustil, lebo ani na php.net to neudavaju ako povinnu hodnotu a aj podla tejto dalsej stranky vela zalezi na HW kde bezi server ;-)

 
Odpovědět 4. listopadu 23:48
Avatar
Marek Z.
Redaktor
Avatar
Odpovídá na Peter Schoeller
Marek Z.:5. listopadu 0:47

Pouze jsem navrhl, že to je ideální hranice. +- sekunda na víc u přihlašování nikoho nezabije.
Defaultně je 10 a to je moc rychlé.. :/

Odpovědět  +1 5. listopadu 0:47
Chybami se člověk učí, běžte se učit jinam!
Avatar
Odpovídá na Marek Z.
Peter Schoeller:5. listopadu 11:22

Aha.... Tak ak je to otazka sekundy, dvoch, mas pravdu, treba to doplnit.
Ja mam na stranke aj "brzdu" v podobe reCAPTCHA. :-)

 
Odpovědět  +1 5. listopadu 11:22
Avatar
Marek Z.
Redaktor
Avatar
Odpovídá na Peter Schoeller
Marek Z.:5. listopadu 16:36

Ale jak jsi psal, záleží na HW, může ti to počítat rychleji nebo i pomaleji.

Odpovědět 5. listopadu 16:36
Chybami se člověk učí, běžte se učit jinam!
Avatar
Marek Hüttl
Člen
Avatar
Marek Hüttl:5. listopadu 20:45

Ahoj, když kliknu na tlačítko restartovat tak se nic neděje, tabulka se "vynuluje" a v myphpadmin se nic nezapíše.. :/ v Php jsem laik..

<?php
session_start();
require('Db.php');
Db::connect('127.0.0.1', 'ners_db', 'root', '');

if ($_POST)
{
        if ($_POST['rok'] != date('2017'))
                $zprava = 'Chybně vyplněný antispam.';
        else if ($_POST['heslo'] != $_POST['heslo_znovu'])
                $zprava = 'Hesla nesouhlasí';
        else
        {
                $existuje = Db::querySingle('
                        SELECT COUNT(*)
                        FROM uzivatele
                        WHERE jmeno=?
                        LIMIT 1
                ', $_POST['jmeno']);
                if ($existuje)
                        $zprava = 'Uživatel s touto přezdívkou je již v databázi obsažen.';
                else
                {
                        Db::query('
                                INSERT INTO uzivatele (jmeno, heslo)
                                VALUES (?, SHA1(?))
                        ', $_POST['jmeno'], $_POST['heslo'] . "t&#ssdf54gh");
                        $_SESSION['uzivatel_id'] = Db::getLastId();
                        $_SESSION['uzivatel_jmeno'] = $_POST['jmeno'];
                        $_SESSION['uzivatel_admin'] = 0;
                        header('Location: administrace.php');
                        exit();
                }
        }
}
?>

<!DOCTYPE html>
<html lang="cs-cz">
<head>
        <link rel="stylesheet" href="styl.css" type="text/css" />
        <title>Registrace</title>
</head>

<body>
        <article>
                <div id="centrovac">
                        <header>
                                <h1>Registrace</h1>
                        </header>
                        <section>
                                <?php
                                        if (isset($zprava))
                                                echo('<p>' . $zprava . '</p>');
                                ?>

                                <form method="post">
                                        Jméno<br />
                                        <input type="text" name="jmeno" /><br />
                                        Heslo<br />
                                        <input type="password" name="heslo" /><br />
                                        Heslo znovu<br />
                                        <input type="password" name="heslo_znovu" /><br />
                                        Zadejte aktuální rok (antispam)<br />
                                        <input type="text" name="rok" /><br />
                                        <input type="submit" value="Registrovat" />
                                </form>
                        </section>
                        <div class="cistic"></div>
                </div>
        </article>
</body>
</html>

Db.php

<?php

/**
 *             __          __                __
 *        ____/ /__ _   __/ /_  ____  ____  / /__ _________
 *       / __  / _ \ | / / __ \/ __ \/ __ \/ //_// ___/_  /
 *      / /_/ /  __/ |/ / /_/ / /_/ / /_/ / ,< _/ /__  / /_
 *      \__,_/\___/|___/_.___/\____/\____/_/|_(_)___/ /___/
 *
 *
 *      TUTORIÁLY  <>  DISKUZE  <>  KOMUNITA  <>  SOFTWARE
 *
 *      Tento zdrojový kód je součástí tutoriálů na programátorské
 *      sociální síti WWW.DEVBOOK.CZ
 *
 *      Kód můžete upravovat jak chcete, jen zmiňte odkaz
 *      na www.devbook.cz :-)
 *
 *  Jednoduchý databázový wrapper nad PDO
 */
class Db
{
        /**
         * @var PDO Databázové spojení
         */
        private static $connection;

        /**
         * @var array Výchozí nastavení ovladače
         */
        private static $options = array(
                PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING,
                PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8",
                PDO::ATTR_EMULATE_PREPARES => false,
        );

        /**
         * Připojí se k databázi pomocí daných údajů
         * @param string $host Název hostitele
         * @param string $database Název databáze
         * @param string $user Uživatelské jméno
         * @param string $password Heslo
         */
        public static function connect($host, $database, $user, $password)
        {
                if (!isset(self::$connection)) {
                        $dsn = "mysql:host=$host;dbname=$database";
                        self::$connection = new PDO($dsn, $user, $password, self::$options);
                }
        }

        /**
         * Spustí dotaz a vrátí PDO statement
         * @param array $params Pole, kde je prvním prvkem dotaz a dalšími jsou parametry
         * @return \PDOStatement PDO statement
         */
        private static function executeStatement($params)
        {
                $query = array_shift($params);
                $statement = self::$connection->prepare($query);
                $statement->execute($params);
                return $statement;
        }

        /**
         * Spustí dotaz a vrátí počet ovlivněných řádků. Dále se předá libovolný počet dalších parametrů.
         * @param string $query Dotaz
         * @return int Počet ovlivněných řádků
         */
        public static function query($query) {
                $statement = self::executeStatement(func_get_args());
                return $statement->rowCount();
        }

        /**
         * Spustí dotaz a vrátí z něj první sloupec prvního řádku. Dále se předá libovolný počet dalších parametrů.
         * @param string $query Dotaz
         * @return mixed Hodnota prvního sloupce z prvního řádku
         */
        public static function querySingle($query) {
                $statement = self::executeStatement(func_get_args());
                $data = $statement->fetch();
                return $data[0];
        }

        /**
         * Spustí dotaz a vrátí z něj první řádek. Dále se předá libovolný počet dalších parametrů.
         * @param string $query Dotaz
         * @return mixed Pole výsledků nebo false při neúspěchu
         */
        public static function queryOne($query) {
                $statement = self::executeStatement(func_get_args());
                return $statement->fetch(PDO::FETCH_ASSOC);
        }

        /**
         * Spustí dotaz a vrátí všechny jeho řádky jako pole asociativních polí. Dále se předá libovolný počet dalších parametrů.
         * @param string $query Dotaz
         * @return mixed Pole řádků enbo false při neúspěchu
         */
        public static function queryAll($query) {
                $statement = self::executeStatement(func_get_args());
                return $statement->fetchAll(PDO::FETCH_ASSOC);
        }

        /**
         * Umožňuje snadné vložení záznamu do databáze pomocí asociativního pole
         * @param string $table Název tabulky
         * @param array $data Asociativní pole, kde jsou klíče sloupce a hodnoty hodnoty
         * @return int Počet ovlivněných řádků
         */
        public static function insert($table, $data) {
                $keys = array_keys($data);
                self::checkIdentifiers(array($table) + $keys);
                $query = "
                        INSERT INTO `$table` (`" . implode('`, `', $keys) . "`)
                        VALUES (" . str_repeat('?,', count($data) - 1) . "?)
                ";
                $params = array_merge(array($query), array_values($data));
                $statement = self::executeStatement($params);
                return $statement->rowCount();
        }

        /**
         * Umožňuje snadnou modifikaci záznamu v databázi pomocí asociativního pole
         * @param string $table Název tabulky
         * @param array $data Asociativní pole, kde jsou klíče sloupce a hodnoty hodnoty
         * @param string $condition Řetězec s SQL podmínkou (WHERE)
         * @return mixed
         */
        public static function update($table, $data, $condition) {
                $keys = array_keys($data);
                self::checkIdentifiers(array($table) + $keys);
                $query = "
                        UPDATE `$table` SET `".
                        implode('` = ?, `', array_keys($data)) . "` = ?
                        $condition
                ";
                $params = array_merge(array($query), array_values($data), array_slice(func_get_args(), 3));
                $statement = self::executeStatement($params);
                return $statement->rowCount();
        }

        /**
         * Vrátí poslední ID posledního záznamu vloženého pomocí INSERT
         * @return mixed Id posledního záznamu
         */
        public static function getLastId()
        {
                return self::$connection->lastInsertId();
        }

        /**
         * Ošetří string proti SQL injekci
         * @param string $string Řetězec
         * @return mixed Ošetřený řetězec
         */
        public static function quote($string)
        {
                return self::$connection->quote($string);
        }

        /**
         * Zkontroluje, zda identifikátory odpovídají formátu identifikátorů
         * @param array $identifiers Pole identifikátorů
         * @throws \Exception
         */
        private static function checkIdentifiers($identifiers)
        {
                foreach ($identifiers as $identifier)
                {
                        if (!preg_match('/^[a-zA-Z0-9\_\-]+$/u', $identifier))
                                throw new Exception('Dangerous identifier in SQL query');
                }
        }
}
 
Odpovědět 5. listopadu 20:45
Avatar
Odpovídá na Marek Hüttl
Peter Schoeller:19. listopadu 11:59

Ahoj
Asi si myslel "registrovat" namiesto "restartovat" ;-)
Ale k veci:
Mas dobre nastaveny pristup k databazi?

Db::connect('127.0.0.1', 'ners_db', 'root', '');

Hlavne treti a stvrty argument, kde je meno a heslo.
Lebo ak tvoj php je presne ako si ho napisal sem, tak na pozicii hesla nemas vobec nic, a preto sa ani nic nedeje.
Predpokladam ze tabulku s nazvom "ners_db" mas vytvorenu spravne.
Db.php sem nemusis pisat, lebo ten je urcite v poriadku.

 
Odpovědět 19. listopadu 11:59
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 87. Zobrazit vše