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.

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.

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);
?>

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.';
}
?>

Preverjanje reset povezave

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

  • preverimo parametre v povezavi,
  • v bazi poiščemo ustrezen zahtevek za reset,
  • preverimo, ali žeton ustreza,
  • preverimo, ali zahtevek še ni potekel,
  • veljavne podatke začasno shranimo v $_SESSION.

Varnost pri zamenjavi gesla

  • novo geslo mora biti dovolj dolgo,
  • obe vneseni gesli se morata ujemati,
  • novo geslo pred shranjevanjem pretvorimo v hash,
  • po uspešni uporabi reset zahtevek izbrišemo,
  • po koncu počistimo podatke iz seje in uporabnika preusmerimo na prijavo.

📘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 zgošči 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

Navodila za izdelavo zamenjave gesla

  1. Iz povezave preberemo identifikator uporabnika, identifikator zahtevka in žeton.
  2. Preverimo, ali so vsi podatki veljavni.
  3. V bazi poiščemo ustrezen reset zahtevek.
  4. Preverimo, ali zahtevek še ni potekel.
  5. Če je zahtevek veljaven, uporabniku prikažemo obrazec za novo geslo.
  6. Preverimo ujemanje in dolžino novega gesla.
  7. Novo geslo zgoščimo s funkcijo password_hash() in posodobimo tabelo users.
  8. Po uspešni spremembi izbrišemo reset zahtevek, počistimo sejo in uporabnika preusmerimo na prijavo.

Pri učenju je smiselno poznati oba pristopa:

  • mysqli za klasično izvedbo preverjanja in posodobitve gesla,
  • PDO za sodobnejši pristop s pripravljenimi poizvedbami,
  • aplikacijski pristop, kjer sta preverjanje povezave in nastavitev novega gesla ločena koraka.