Parollar — password_hash, password_verify, ro'yxatdan o'tish

~20 daq · PHP · Ma'lumotlar bazasi

Muammodan boshlaylik

Saytingiz bazasi o'g'irlandi. Bu har kuni kimningdir boshiga tushadi — SQL injection, eskirgan server, o'g'irlangan backup fayl, norozi xodim.

Agar foydalanuvchilar jadvalida parollar ochiq saqlangan bo'lsa:

| email           | parol        |
|-----------------|--------------|
| ali@mail.uz     | Ali1995!     |
| malika@mail.uz  | qwerty123    |

Hujumchi nafaqat sizning saytingizga kiradi. Odamlarning ko'pchiligi bir xil parolni hamma joyda ishlatadi. Endi hujumchi ularning pochtasi, Telegram i, bank ilovasiga ham kiradi. Sizning xatoyingiz — ularning halokati.

Qoida: parol hech qachon, hech qanday ko'rinishda ochiq saqlanmaydi. Hatto admin ham uni ko'ra olmasligi kerak.


Xeshlash nima?

Xesh — bir tomonlama funksiya: paroldan "barmoq izi" yasaydi, lekin barmoq izidan parolni tiklab bo'lmaydi.

"qwerty123"  →  $2y$12$Kx8f2...Lq9W   (har safar boshqacha, 60 belgi)

Login paytida biz parolni "ochmaymiz" — kiritilgan parolni ham xeshlab, natijalarni solishtiramiz.

Hayotiy o'xshatma: go'sht maydalagich. Go'shtdan qiyma qilish oson. Qiymadan go'shtni qaytarib yig'ish — imkonsiz. Lekin bir xil go'shtni bir xil usulda maydalasangiz — bir xil qiyma chiqadi.


password_hash — xeshlash

<?php

$xesh = password_hash('qwerty123', PASSWORD_DEFAULT);
echo $xesh;
// $2y$12$Y3oGRsQd5k2NqH8nVhE7FOj6K0aD3.rQ1nHbKJvM3sLxX9p2wTq0e

Xesh ichida hamma narsa bor:

$2y$  12$  Y3oGRsQd5k2NqH8nVhE7FO  j6K0aD3.rQ1nHbKJvM3sLxX9p2wTq0e
│     │    │                       │
│     │    tuz (salt) — tasodifiy  natija
│     qiyinlik (cost)
algoritm (bcrypt)

Ikki muhim xususiyat:

  • Tuz (salt) — har safar yangi tasodifiy qiymat. Bir xil parol ikki marta xeshlansa ham turli natija chiqadi. Hujumchi oldindan tayyorlangan "parol → xesh" jadvallaridan foydalana olmaydi.
  • Sekinlik — bcrypt ataylab sekin (~0.1-0.3 soniya). Foydalanuvchi uchun sezilmaydi, lekin hujumchi milliardlab parolni sinab ko'rishi uchun yuzlab yillar kerak bo'ladi.

password_verify — tekshirish

<?php

$xesh = '$2y$12$Y3oGRsQd5k2NqH8nVhE7FOj6K0aD3.rQ1nHbKJvM3sLxX9p2wTq0e';

var_dump(password_verify('qwerty123', $xesh));   // bool(true)
var_dump(password_verify('qwerty124', $xesh));   // bool(false)

password_verify xeshdan algoritm, cost va tuzni o'zi o'qiydi. Sizga hech narsa kerak emas.

[!WARNING] Hech qachon md5(), sha1(), sha256 yoki hash() bilan parol xeshlamang. Ular tez ishlash uchun yaratilgan — oddiy videokarta sekundiga milliardlab MD5 hisoblaydi. Internetdagi "PHP login" darsliklarining ko'pchiligi hali ham md5($parol) ni ko'rsatadi — bu 2004-yilgi usul.


Jadval

CREATE TABLE foydalanuvchilar (
    id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ism         VARCHAR(100) NOT NULL,
    email       VARCHAR(255) NOT NULL UNIQUE,
    parol_hash  VARCHAR(255) NOT NULL,
    yaratilgan  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
  • email ... UNIQUE — bir email bilan ikki marta ro'yxatdan o'tib bo'lmaydi (baza o'zi tekshiradi)
  • parol_hash VARCHAR(255) — hozir 60 belgi, lekin kelajakdagi algoritmlar uzunroq bo'lishi mumkin

Ro'yxatdan o'tish

<?php
declare(strict_types=1);
session_start();

require __DIR__ . '/../src/db.php';
require __DIR__ . '/../src/yordamchi.php';

$xatolar = [];
$ism = $email = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $ism   = trim($_POST['ism'] ?? '');
    $email = mb_strtolower(trim($_POST['email'] ?? ''));
    $parol = $_POST['parol'] ?? '';
    $takror = $_POST['parol_takror'] ?? '';

    if (mb_strlen($ism) < 2) {
        $xatolar[] = "Ism kamida 2 belgi";
    }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $xatolar[] = "Email noto'g'ri";
    }
    if (strlen($parol) < 8) {
        $xatolar[] = "Parol kamida 8 belgi";
    }
    if ($parol !== $takror) {
        $xatolar[] = "Parollar mos emas";
    }

    if (!$xatolar) {
        try {
            $stmt = db()->prepare(
                'INSERT INTO foydalanuvchilar (ism, email, parol_hash) VALUES (?, ?, ?)'
            );
            $stmt->execute([$ism, $email, password_hash($parol, PASSWORD_DEFAULT)]);

            session_regenerate_id(true);
            $_SESSION['user_id'] = (int) db()->lastInsertId();
            yonaltir('/profil.php');
        } catch (PDOException $e) {
            if ($e->errorInfo[1] === 1062) {   // MySQL: Duplicate entry
                $xatolar[] = "Bu email bilan allaqachon ro'yxatdan o'tilgan";
            } else {
                throw $e;
            }
        }
    }
}
?>
<form method="post">
    <?php foreach ($xatolar as $x): ?><p style="color:red"><?= e($x) ?></p><?php endforeach; ?>
    <input name="ism" value="<?= e($ism) ?>" placeholder="Ism">
    <input name="email" type="email" value="<?= e($email) ?>" placeholder="Email">
    <input name="parol" type="password" placeholder="Parol">
    <input name="parol_takror" type="password" placeholder="Parolni takrorlang">
    <button>Ro'yxatdan o'tish</button>
</form>

Email bandligini avval SELECT bilan tekshirish o'rniga — UNIQUE cheklovga tayanamiz va 1062 xatosini ushlaymiz. Nima uchun? Ikki odam bir soniyada bir xil email bilan yuborsa, SELECT ikkalasiga ham "bo'sh" deydi. UNIQUE esa bazaning o'zida — uni aylanib o'tib bo'lmaydi.


Kirish (login)

<?php
declare(strict_types=1);
session_start();

require __DIR__ . '/../src/db.php';
require __DIR__ . '/../src/yordamchi.php';

$xato = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = mb_strtolower(trim($_POST['email'] ?? ''));
    $parol = $_POST['parol'] ?? '';

    $stmt = db()->prepare('SELECT id, parol_hash FROM foydalanuvchilar WHERE email = ?');
    $stmt->execute([$email]);
    $user = $stmt->fetch();

    if ($user && password_verify($parol, $user['parol_hash'])) {
        // Algoritm yoki cost yangilangan bo'lsa — xeshni yangilash
        if (password_needs_rehash($user['parol_hash'], PASSWORD_DEFAULT)) {
            db()->prepare('UPDATE foydalanuvchilar SET parol_hash = ? WHERE id = ?')
                ->execute([password_hash($parol, PASSWORD_DEFAULT), $user['id']]);
        }

        session_regenerate_id(true);
        $_SESSION['user_id'] = (int) $user['id'];
        yonaltir('/profil.php');
    }

    $xato = "Email yoki parol noto'g'ri";
}

Nima uchun "Email yoki parol noto'g'ri"?

"Bunday email topilmadi" va "Parol noto'g'ri" deb alohida xabar bersangiz — hujumchi qaysi emaillar saytingizda ro'yxatdan o'tganini bilib oladi. Keyin aynan shu emaillarga hujum qiladi yoki fishing xat yuboradi. Umumiy xabar — hech qanday ma'lumot bermaydi.

password_needs_rehash

Yillar o'tib kompyuterlar tezlashadi. PHP yangi versiyasida PASSWORD_DEFAULT ning cost i oshadi yoki algoritmi o'zgaradi. password_needs_rehash foydalanuvchi navbatdagi marta kirganda (shu paytda bizda ochiq parol bor) xeshini avtomatik yangilaydi. Hech kimdan parolni qayta so'rash shart emas.


Brute force dan himoya

Hujumchi bitta emailga daqiqasiga minglab parol sinab ko'rishi mumkin. Oddiy himoya — urinishlarni cheklash:

CREATE TABLE kirish_urinishlari (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email      VARCHAR(255) NOT NULL,
    ip         VARCHAR(45) NOT NULL,
    vaqt       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX (email, vaqt)
);
<?php

$stmt = db()->prepare('
    SELECT COUNT(*) FROM kirish_urinishlari
    WHERE email = ? AND vaqt > NOW() - INTERVAL 15 MINUTE
');
$stmt->execute([$email]);

if ($stmt->fetchColumn() >= 5) {
    $xato = "Juda ko'p urinish. 15 daqiqadan keyin qayta urinib ko'ring.";
} else {
    // ... login tekshiruvi; muvaffaqiyatsiz bo'lsa — urinishni yozib qo'yish:
    db()->prepare('INSERT INTO kirish_urinishlari (email, ip) VALUES (?, ?)')
        ->execute([$email, $_SERVER['REMOTE_ADDR']]);
}

Nima uchun shunday?

Nima uchun xeshlash algoritmi ataylab sekin bo'lishi kerak — odatda dasturlashda tezlikka intilmaymizmi?

Chunki bu yerda tezlik hujumchiga xizmat qiladi. Foydalanuvchi bir marta login qiladi — 0.2 soniya sezilmaydi. Hujumchi esa o'g'irlangan bazadagi har bir xesh uchun millionlab parolni sinaydi. SHA-256 bilan bu bir necha soatlik ish. bcrypt bilan esa xuddi shu ish ming yillar talab qiladi. cost parametri esa yillar o'tib, kompyuterlar tezlashgani sari sekinlikni oshirish imkonini beradi.


Xulosa

Eslab qoling:

  • Parol hech qachon ochiq saqlanmaydi
  • password_hash($parol, PASSWORD_DEFAULT) — saqlash
  • password_verify($parol, $xesh) — tekshirish
  • md5, sha1, sha256 — parol uchun yaroqsiz
  • parol_hash VARCHAR(255), email UNIQUE
  • Login xatosi: "Email yoki parol noto'g'ri" — qaysi biri ekanini aytmang
  • password_needs_rehash — xeshlarni avtomatik yangilash
  • Urinishlar sonini cheklang

Keyingi dars

39-dars: Composer — paketlar, composer.json, vendor/autoload.php