2 Ekim 2026 - 10:17
PHP ile Tek Kullanımlık Parola Sıfırlama Bağlantısı Görseli
PHP

PHP ile Tek Kullanımlık Parola Sıfırlama Bağlantısı

Yorumlar

Parola sıfırlama bağlantısı bir kullanıcı adı taşıyan sıradan adres değildir. Elinde bağlantı bulunan kişi hesabın parolasını değiştirebildiği için, bağlantıdaki rastgele değer geçici bir anahtar gibi ele alınmalıdır. Süresinin dolması, kullanıldığında geçersizleşmesi ve aynı anda gelen iki isteğin ikisinin birden başarılı olamaması gerekir.

Bu örnek HTTPS kullanan PHP uygulamasında PDO ile MySQL veya MariaDB'ye bağlanan bir sıfırlama akışının veritabanı çekirdeğidir. SMTP kurulumu ayrı konudur; gönderim tarafını PHPMailer yazısıyla tamamlayabilirsiniz. Aşağıdaki tablolar blog içerik tablosu için değildir. Önce ayrı bir test veritabanında çalışın.

Kullanıcı tablosuyla token tablosunu ayırın

Örnekte users tablosunda id BIGINT UNSIGNED, password_hash VARCHAR(255), is_active TINYINT ve auth_version INT UNSIGNED NOT NULL DEFAULT 1 alanları bulunduğunu varsayıyoruz. remember_tokens tablosu ise beni hatırla yazısındaki yapıdır. Alan adlarını kendi şemanıza uyarlayın.

SQL
CREATE TABLE password_resets (
    token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    expires_at DATETIME NOT NULL,
    created_at DATETIME NOT NULL,
    INDEX idx_reset_user (user_id),
    INDEX idx_reset_expiry (expires_at)
) ENGINE=InnoDB;

Ham token e-postadaki bağlantıya gider, veritabanında yalnızca özeti bulunur. Rastgele tokenın özetini saklamak, veritabanındaki değerin doğrudan bağlantı olarak kullanılmasını önler. Kullanıcının parolasıyla token aynı şey değildir; parola için hızlı SHA-256 yerine parola saklamaya uygun password_hash() kullanılır.

Talep alma ve bağlantı üretme

Formdaki e-posta adresini doğrulayın ve hesabı sunucuda bulun. Yanıtınız hesap bulunsa da bulunmasa da aynı olsun: “Adres sistemimizde kayıtlıysa sıfırlama bağlantısı gönderilecektir.” Gönderimi kuyruk üzerinden yapmak, SMTP gecikmesinin hesap varlığını dışarı belli etmesini azaltır. Hesap ve IP bazında istek sınırı koyun; aynı kişiye arka arkaya yüzlerce e-posta gönderilmesine izin vermeyin.

Aşağıdaki parça, hesap bulunduktan sonra çalışır. $userId sorgudan gelir; ziyaretçinin gönderdiği üye numarası değildir. Örnekte 30 dakikalık kullanım süresi seçilmiştir. PDO bağlantısında istisna modu açık olmalıdır.

PHP
<?php
$token = bin2hex(random_bytes(32));
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$stmt = $pdo->prepare(
    'INSERT INTO password_resets
    (token_hash, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)'
);
$stmt->execute([
    hash('sha256', $token), $userId,
    $now->modify('+30 minutes')->format('Y-m-d H:i:s'),
    $now->format('Y-m-d H:i:s'),
]);
$link = 'https://example.com/parola-yenile?token=' . rawurlencode($token);

example.com yerine dağıtım yapılandırmasındaki sabit, güvenilir adresi kullanın. Bağlantıyı ziyaretçinin Host başlığından üretmeyin. Tokenı uygulama loglarında, hata mesajlarında veya üçüncü taraf analiz araçlarında tutmayın. Sıfırlama sayfasında Referrer-Policy: no-referrer ve Cache-Control: no-store başlıklarını kullanın; erişim loglarında da bu rotanın sorgu parametresini maskeleyin.

GET bağlantıyı açar, POST parolayı değiştirir

E-posta güvenlik servisleri bağlantıları önceden ziyaret edebilir. Bu nedenle sayfanın GET isteğiyle açılması tokenı tüketmemeli ve hesabı değiştirmemelidir. GET yalnızca formu gösterir. Kullanıcı yeni parolayı gönderdiğinde POST isteğini, oturuma bağlı CSRF değerini ve token biçimini denetleyin.

Parola politikası kayıt ve parola değiştirme ekranlarında tutarlı olmalıdır. Aşağıdaki çekirdek, örnek olarak 15 karakter altını ve 72 bayt üstünü reddeder. Üst sınır, PASSWORD_DEFAULT ile bcrypt kullanılan ortamlarda sessiz kesilmeyi önlemek içindir; bütün sistemlerde evrensel bir parola üst sınırı değildir. Argon2id kullanıyorsanız politikayı ve kaynak maliyetini o algoritmaya göre belirleyin. Parolayı trim() ile değiştirmeyin.

PHP
<?php
declare(strict_types=1);
function resetPassword(PDO $pdo, string $token, string $password): bool
{
    if (!preg_match('/\A[a-f0-9]{64}\z/D', $token)) {
        return false;
    }
    if (!mb_check_encoding($password, 'UTF-8')
        || mb_strlen($password, 'UTF-8') < 15
        || strlen($password) > 72
        || str_contains($password, "\0")) {
        throw new InvalidArgumentException('Parola politikasina uymuyor.');
    }
    $digest = hash('sha256', $token);
    $find = $pdo->prepare(
        'SELECT user_id FROM password_resets WHERE token_hash = ?'
    );
    $find->execute([$digest]);
    $userId = $find->fetchColumn();
    if ($userId === false) {
        return false;
    }
    // Pahali parola hesaplamasi kilit tutulmadan once yapilir.
    $newHash = password_hash($password, PASSWORD_DEFAULT);
    $pdo->beginTransaction();
    try {
        // Butun hesap islemlerinde once kullanici satirini kilitleyin.
        $user = $pdo->prepare(
            'SELECT id, is_active FROM users WHERE id = ? FOR UPDATE'
        );
        $user->execute([$userId]);
        $account = $user->fetch(PDO::FETCH_ASSOC);
        $query = $pdo->prepare(
            'SELECT user_id, expires_at FROM password_resets
             WHERE token_hash = ? FOR UPDATE'
        );
        $query->execute([$digest]);
        $row = $query->fetch(PDO::FETCH_ASSOC);
        $now = gmdate('Y-m-d H:i:s');
        if (!$account || (int) $account['is_active'] !== 1 || !$row
            || (int) $row['user_id'] !== (int) $userId
            || $row['expires_at'] <= $now) {
            $pdo->rollBack();
            return false;
        }
        $update = $pdo->prepare(
            'UPDATE users SET password_hash = ?,
             auth_version = auth_version + 1 WHERE id = ?'
        );
        $update->execute([$newHash, $userId]);
        $pdo->prepare('DELETE FROM password_resets WHERE user_id = ?')
            ->execute([$userId]);
        $pdo->prepare('DELETE FROM remember_tokens WHERE user_id = ?')
            ->execute([$userId]);
        $pdo->commit();
        return true;
    } catch (Throwable $error) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $error;
    }
}

Fonksiyonu çağıran denetleyici CSRF ve hız sınırını önce uygular. Beklenen geçersiz token için false döner; veritabanı arızasını aynı şeymiş gibi yutmaz. Hata ayrıntısını ziyaretçiye göstermeden sunucu tarafında ilişkilendirme numarasıyla kaydedin. Bu kod InnoDB satır kilitlerine dayanır; bağlantıları ve tablo motorunu buna göre hazırlayın.

İlk sorgu yalnızca hangi hesabın kilitleneceğini bulur. Yetki veren asıl token kontrolü işlem içinde yeniden yapılır. Aynı bağlantıyla ikinci istek gelirse kullanıcı kilidini bekler; ilk işlem tokenları silip tamamlandıktan sonra geçerli kayıt bulamaz. Parola güncellemesi ve token silme birlikte commit edilir. Bağlantı kesilip sonuç belirsizleşirse kullanıcı yeni parolasıyla giriş deneyebilir veya yeni bağlantı isteyebilir.

Eski oturumlar ne olacak?

Parola değişince yalnızca parola alanını güncellemek mevcut PHP oturumlarını kapatmaz. Burada artırılan auth_version değerini korunan her istekte kullanıcı satırıyla karşılaştırmanız gerekir; bunun uygulaması tüm cihazlardan çıkış yazısında var. Başarılı sıfırlamadan sonra otomatik giriş vermeyin; kullanıcıyı normal giriş akışına döndürün ve parola değişikliği bildirimi gönderin. Bildirim e-postasına yeni parolayı yazmayın.

Başvuru: OWASP parola sıfırlama rehberi ve PHP password_hash.

Test hesabında geçmesi gereken denemeler

  • Geçerli bağlantı ve politikaya uygun parola: işlem bir kez başarılı olur.
  • Aynı bağlantıyla ikinci POST: parola yeniden değişmez.
  • Süresi geçmiş veya tek karakteri değiştirilmiş token: işlem reddedilir.
  • Aynı bağlantıyla paralel iki POST: yalnızca biri başarılı olur.
  • Başarılı sıfırlama sonrası eski parola ve eski kalıcı giriş tokenı: erişim vermez.
  • İşlem ortasında veritabanı hatası: parola ve token silme işlemleri birlikte geri alınır.

Benzer Yazılar

Yorumlar ()

Henüz yorum yok. İlk yorum yapan sen ol!

Yorum Yap