Načrtovanje in razvoj spletnih aplikacij

Zamenjava gesla

Zamenjava gesla uporabniku omogoča, da si po pozabljenem ali potečenem geslu nastavi novo varno geslo. Pri tem moramo najprej preveriti, ali je povezava za ponastavitev veljavna, nato pa novo geslo varno shraniti v bazo.

Postopek zamenjave gesla je varnostno občutljiv, ker uporabniku omogoča dostop do računa brez starega gesla. Zato moramo preveriti identifikator uporabnika, identifikator zahtevka, žeton, čas veljavnosti povezave in pravilnost novega gesla.

Pomni: Novo geslo pred shranjevanjem vedno pretvorimo v hash s funkcijo password_hash(). Gesla ne shranjujemo v navadnem besedilu.

Reset zahtevek naj bo časovno omejen. V prikazanem zgledu je povezava veljavna 30 minut, nato pa je ne smemo več uporabiti za zamenjavo gesla.

Vsebina strani

Osnovna pravila

Zamenjava gesla je običajno sestavljena iz dveh ločenih delov. Najprej preverimo povezavo za ponastavitev gesla, nato pa uporabniku omogočimo vnos novega gesla.

  • Iz povezave preberemo identifikator uporabnika, identifikator zahtevka in žeton.
  • Vrednosti iz povezave preverimo kot nezaupan uporabniški vnos.
  • V tabeli password_reset_request poiščemo ustrezen reset zahtevek.
  • Preverimo, ali zahtevek še ni potekel.
  • Veljavne podatke začasno shranimo v $_SESSION.
  • Uporabniku prikažemo obrazec za novo geslo.
  • Novo geslo preverimo in ga pretvorimo v hash.
  • Geslo posodobimo v tabeli users.
  • Uporabljeni reset zahtevek izbrišemo.

Pozor: Reset žeton je občutljiv podatek. Ne shranjujemo ga v javne izpise in ga po uspešni uporabi odstranimo oziroma izbrišemo skupaj z zahtevkom.

Zamenjava gesla uporabnika

Zamenjava gesla je običajno del postopka za obnovitev dostopa do uporabniškega računa. Uporabnik po e-pošti prejme posebno povezavo, ki vsebuje identifikacijske podatke zahtevka za reset.

Postopek navadno poteka v dveh korakih:

  1. preverimo, ali je povezava za reset veljavna,
  2. uporabniku prikažemo obrazec za vnos novega gesla.

Po uspešni nastavitvi novega gesla staro geslo ne velja več, novo geslo pa moramo pred shranjevanjem pretvoriti v varen hash.

Pri posodobitvi gesla uporabimo ukaz:

UPDATE users
SET password = :password
WHERE id = :id;

Po uspešni zamenjavi gesla uporabljeni reset zahtevek odstranimo:

DELETE FROM password_reset_request
WHERE id = :id;

Pomni: Reset zahtevek izbrišemo zato, da iste povezave ni mogoče uporabiti večkrat.

Osnovni primer z mysqli

Spodnji zgled prikazuje osnovni postopek zamenjave gesla z uporabo mysqli: preverjanje reset zahtevka, preverjanje novega gesla in posodobitev uporabnika v tabeli users.

<?php
session_start();

define('DB_SERVER', 'localhost');
define('DB_USER', 'uporabnik');
define('DB_PASS', 'skritoGeslo');
define('DB_NAME', 'knjiznica');

$connection = mysqli_connect(DB_SERVER, DB_USER, DB_PASS, DB_NAME);

if (!$connection) {
    die(
        'Povezava s podatkovno zbirko ni vzpostavljena: ' .
        mysqli_connect_error() .
        ' (' . mysqli_connect_errno() . ')'
    );
}

// 1. korak: preverjanje povezave za reset
$userId = $_GET['uid'] ?? '';
$token = $_GET['t'] ?? '';
$requestId = $_GET['id'] ?? '';

if (
    filter_var($userId, FILTER_VALIDATE_INT) !== false &&
    filter_var($requestId, FILTER_VALIDATE_INT) !== false &&
    trim((string)$token) !== ''
) {
    $stmt = mysqli_prepare(
        $connection,
        "SELECT id, user_id, date_requested, token
         FROM password_reset_request
         WHERE user_id = ? AND token = ? AND id = ?"
    );
    mysqli_stmt_bind_param($stmt, 'isi', $userId, $token, $requestId);
    mysqli_stmt_execute($stmt);
    $result = mysqli_stmt_get_result($stmt);
    $requestInfo = mysqli_fetch_assoc($result);

    if ($requestInfo) {
        $requestedAt = strtotime($requestInfo['date_requested']);

        if ($requestedAt !== false && (time() - $requestedAt) <= 1800) {
            $_SESSION['reset_pwd_user_id'] = (int)$requestInfo['user_id'];
            $_SESSION['reset_pwd_request_id'] = (int)$requestInfo['id'];
            $_SESSION['reset_pwd_token'] = $token;
        }
    }

    mysqli_stmt_close($stmt);
}

// 2. korak: nastavitev novega gesla
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $password = $_POST['pwd'] ?? '';
    $rePassword = $_POST['re-pwd'] ?? '';

    if (
        $password !== '' &&
        $rePassword !== '' &&
        $password === $rePassword &&
        mb_strlen($password) >= 8 &&
        isset($_SESSION['reset_pwd_user_id'])
    ) {
        $passwordHash = password_hash($password, PASSWORD_DEFAULT);

        $stmt = mysqli_prepare(
            $connection,
            "UPDATE users SET password = ? WHERE id = ?"
        );
        mysqli_stmt_bind_param($stmt, 'si', $passwordHash, $_SESSION['reset_pwd_user_id']);
        mysqli_stmt_execute($stmt);
        mysqli_stmt_close($stmt);

        $stmt = mysqli_prepare(
            $connection,
            "DELETE FROM password_reset_request WHERE id = ?"
        );
        mysqli_stmt_bind_param($stmt, 'i', $_SESSION['reset_pwd_request_id']);
        mysqli_stmt_execute($stmt);
        mysqli_stmt_close($stmt);

        unset(
            $_SESSION['reset_pwd_user_id'],
            $_SESSION['reset_pwd_request_id'],
            $_SESSION['reset_pwd_token']
        );

        $_SESSION['password-reset'] = 'Vaše geslo je bilo uspešno spremenjeno.';
        header('Location: 15_login.php');
        exit();
    }
}

mysqli_close($connection);
?>

Pri mysqli uporabimo pripravljene stavke za iskanje reset zahtevka, posodobitev gesla in brisanje uporabljenega zahtevka.

Osnovni primer s PDO

Tudi z vmesnikom PDO zamenjavo gesla izvedemo z varnim preverjanjem reset zahtevka, validacijo novega gesla in posodobitvijo hashiranega gesla v tabeli uporabnikov.

<?php
session_start();

$streznik = 'localhost';
$baza = 'knjiznica';
$uporabnik = 'uporabnik';
$geslo = 'skritoGeslo';

try {
    $pdo = new PDO("mysql:host=$streznik;dbname=$baza;charset=utf8mb4", $uporabnik, $geslo);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 1. korak: preverjanje reset povezave
    $userId = $_GET['uid'] ?? '';
    $token = $_GET['t'] ?? '';
    $requestId = $_GET['id'] ?? '';

    if (
        filter_var($userId, FILTER_VALIDATE_INT) !== false &&
        filter_var($requestId, FILTER_VALIDATE_INT) !== false &&
        trim((string)$token) !== ''
    ) {
        $stmt = $pdo->prepare(
            "SELECT id, user_id, date_requested, token
             FROM password_reset_request
             WHERE user_id = :user_id
               AND token = :token
               AND id = :id"
        );
        $stmt->execute([
            ':user_id' => (int)$userId,
            ':token' => $token,
            ':id' => (int)$requestId
        ]);

        $requestInfo = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($requestInfo) {
            $requestedAt = strtotime($requestInfo['date_requested']);

            if ($requestedAt !== false && (time() - $requestedAt) <= 1800) {
                $_SESSION['reset_pwd_user_id'] = (int)$requestInfo['user_id'];
                $_SESSION['reset_pwd_request_id'] = (int)$requestInfo['id'];
                $_SESSION['reset_pwd_token'] = $token;
            }
        }
    }

    // 2. korak: nastavitev novega gesla
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SESSION['reset_pwd_user_id'])) {
        $password = $_POST['pwd'] ?? '';
        $rePassword = $_POST['re-pwd'] ?? '';

        if ($password !== '' && $rePassword !== '' && $password === $rePassword && mb_strlen($password) >= 8) {
            $stmt = $pdo->prepare(
                "SELECT id, user_id, date_requested, token
                 FROM password_reset_request
                 WHERE id = :id
                   AND user_id = :user_id
                   AND token = :token"
            );
            $stmt->execute([
                ':id' => (int)$_SESSION['reset_pwd_request_id'],
                ':user_id' => (int)$_SESSION['reset_pwd_user_id'],
                ':token' => $_SESSION['reset_pwd_token']
            ]);

            $requestInfo = $stmt->fetch(PDO::FETCH_ASSOC);

            if ($requestInfo) {
                $requestedAt = strtotime($requestInfo['date_requested']);

                if ($requestedAt !== false && (time() - $requestedAt) <= 1800) {
                    $passwordHash = password_hash($password, PASSWORD_DEFAULT);

                    $stmt = $pdo->prepare("UPDATE users SET password = :password WHERE id = :id");
                    $stmt->bindValue(':id', (int)$_SESSION['reset_pwd_user_id'], PDO::PARAM_INT);
                    $stmt->bindValue(':password', $passwordHash, PDO::PARAM_STR);
                    $stmt->execute();

                    $stmt = $pdo->prepare("DELETE FROM password_reset_request WHERE id = :id");
                    $stmt->bindValue(':id', (int)$_SESSION['reset_pwd_request_id'], PDO::PARAM_INT);
                    $stmt->execute();

                    unset(
                        $_SESSION['reset_pwd_user_id'],
                        $_SESSION['reset_pwd_request_id'],
                        $_SESSION['reset_pwd_token']
                    );

                    $_SESSION['password-reset'] = 'Vaše geslo je bilo uspešno spremenjeno.';
                    header('Location: 15_login.php');
                    exit();
                }
            }
        }
    }
}
catch (PDOException $e) {
    echo 'Zamenjava gesla trenutno ni mogoča.';
}
?>

Pri PDO uporabimo poimenovane parametre. Pred posodobitvijo gesla reset zahtevek znova preverimo, ker uporabnik obrazec za novo geslo lahko odpre pozneje ali ga poskusi oddati po poteku povezave.

Preverjanje reset povezave

Preden uporabniku dovolimo nastavitev novega gesla, moramo preveriti, ali je povezava za zamenjavo gesla veljavna.

Podatek Preverjanje
uid Identifikator uporabnika mora biti veljavno celo število.
id Identifikator reset zahtevka mora biti veljavno celo število.
t Žeton ne sme biti prazen in se mora ujemati z zapisom v tabeli password_reset_request.
date_requested Datum zahtevka ne sme biti starejši od dovoljenega časa veljavnosti.

Čas veljavnosti lahko preverimo tako:

$requestedAt = strtotime($requestInfo['date_requested']);

if ($requestedAt !== false && (time() - $requestedAt) <= 1800) {
    // zahtevek je še veljaven
}

Pozor: Reset zahtevek preverimo ob odpiranju povezave in ponovno ob oddaji obrazca. Tako preprečimo uporabo potekle povezave, ki je bila odprta še pred potekom.

Preverjanje in shranjevanje novega gesla

Pred shranjevanjem novega gesla preverimo, ali sta obe polji izpolnjeni, ali se gesli ujemata in ali je novo geslo dovolj dolgo.

  • novo geslo ne sme biti prazno,
  • ponovljeno geslo ne sme biti prazno,
  • obe gesli se morata ujemati,
  • geslo mora imeti vsaj določeno minimalno dolžino,
  • pred shranjevanjem uporabimo password_hash().
if ($password === $rePassword && mb_strlen($password) >= 8) {
    $passwordHash = password_hash($password, PASSWORD_DEFAULT);
}

Hashirano geslo nato shranimo v tabelo users:

UPDATE users
SET password = :password
WHERE id = :id;

Pomni: Po uspešni zamenjavi gesla moramo uporabniku omogočiti prijavo z novim geslom, uporabljeni reset zahtevek pa izbrisati.

Primerjava pristopov mysqli in PDO

Oba pristopa omogočata varno zamenjavo gesla s pripravljenimi stavki. Razlika je predvsem v zapisu parametrov in načinu branja rezultatov.

Korak mysqli PDO
Preverjanje reset zahtevka mysqli_prepare() in mysqli_stmt_bind_param() $pdo->prepare() in poimenovani parametri
Branje zahtevka mysqli_fetch_assoc() $stmt->fetch()
Hashiranje gesla password_hash() password_hash()
Posodobitev gesla UPDATE users SET password = ? WHERE id = ? UPDATE users SET password = :password WHERE id = :id
Brisanje zahtevka DELETE FROM password_reset_request WHERE id = ? DELETE FROM password_reset_request WHERE id = :id

Aplikacija Knjige

📘Aplikacija Knjige

V priloženi aplikaciji datoteka 15_zamenjavaPWD.php najprej prebere parametre uid, t in id, preveri njihovo osnovno veljavnost in nato v tabeli password_reset_request poišče ustrezen reset zahtevek. Če zahtevek obstaja in ni starejši od 30 minut, podatke shrani v sejo in uporabnika preusmeri na obrazec za novo geslo.

Datoteka 15_zamenjavaPWD-obrazec.php nato prikaže obrazec za novo geslo. Ob oddaji obrazca znova preveri reset zahtevek, veljavnost časa in ujemanje novega gesla. Nato novo geslo pretvori v varno zgoščeno vrednost s funkcijo password_hash(), posodobi uporabnika v tabeli users in izbriše uporabljeni reset zahtevek.

Po uspešni zamenjavi gesla aplikacija počisti podatke iz seje, nastavi sporočilo za prijavno stran in uporabnika preusmeri nazaj na prijavo. Če je povezava neveljavna ali potekla, se uporabniku prikaže ustrezno opozorilo.

Primer: aplikacija – 15_zamenjavaPWD.php
Primer: aplikacija – 15_zamenjavaPWD-obrazec.php
  1. Iz povezave preberemo identifikator uporabnika, identifikator zahtevka in žeton.
  2. Preverimo, ali so vsi podatki veljavni.
  3. V tabeli password_reset_request poiščemo ustrezen reset zahtevek.
  4. Preverimo, ali zahtevek še ni potekel.
  5. Če je zahtevek veljaven, podatke začasno shranimo v sejo.
  6. Uporabniku prikažemo obrazec za novo geslo.
  7. Ob oddaji obrazca reset zahtevek ponovno preverimo.
  8. Preverimo ujemanje in dolžino novega gesla.
  9. Novo geslo pretvorimo v varno zgoščeno vrednost s funkcijo password_hash() in posodobimo tabelo users.
  10. Po uspešni spremembi izbrišemo reset zahtevek, počistimo sejo in uporabnika preusmerimo na prijavo.

Pozor: Pri uspešni zamenjavi gesla je priporočljivo razveljaviti oziroma izbrisati uporabljeni reset zahtevek. Tako se povezava ne more ponovno uporabiti.

Priporočila

  • Reset povezava naj bo časovno omejena.
  • Žeton naj bo dovolj dolg in naključno ustvarjen.
  • Podatke iz URL naslova vedno preveri.
  • Reset zahtevek preveri ob odpiranju povezave in ob oddaji obrazca.
  • Novo geslo shrani samo kot hash.
  • Po uspešni zamenjavi izbriši uporabljeni reset zahtevek.
  • Po zamenjavi gesla počisti začasne podatke iz seje.
  • Uporabnika preusmeri na prijavno stran in mu prikaži splošno potrditveno sporočilo.

Pogoste napake

  • Reset povezava nima časovne omejitve.
  • Žeton se ne preveri v podatkovni bazi.
  • Reset zahtevek se preveri samo ob odpiranju povezave, ne pa tudi ob oddaji obrazca.
  • Novo geslo se shrani v navadnem besedilu.
  • Po uspešni zamenjavi reset zahtevek ostane v tabeli.
  • Začasni podatki za reset gesla ostanejo v $_SESSION.
  • Pri posodobitvi gesla ni uporabljena pripravljena poizvedba.
  • Po klicu header() manjka exit().

Zamenjava gesla mora preprečiti večkratno uporabo iste povezave. Zato moramo po uspešni spremembi gesla uporabljeni zahtevek odstraniti in počistiti začasne podatke iz seje.