Çıkış düğmesine basınca giriş ekranının görünmesi, oturumun kapandığını kanıtlamaz. Tarayıcıdaki çerez duruyor olabilir, “beni hatırla” tokenı yeni oturum açıyor olabilir veya diğer cihazdaki oturum hâlâ geçerli olabilir. Bu üç durumu ayrı ele almak gerekir: mevcut PHP oturumu, kalıcı giriş kaydı ve hesaba ait bütün oturumlar.
Bu yazı PHP oturumunu sunucuda tutan, HTTPS kullanan bir uygulama içindir. JWT ile tamamen durumsuz kimlik doğrulamada farklı bir iptal tasarımı gerekir. Örnekte users tablosunda id, is_active ve auth_version alanları; kalıcı giriş için de remember_tokens tablosu kullanılır.
Çıkış isteği POST ve CSRF kontrollü olsun
Bir bağlantıyı ziyaret etmek hesap durumunu değiştirmemelidir. Çıkışı GET adresine bağlamak, başka bir sayfanın kullanıcının oturumunu istemeden kapatmasını kolaylaştırır. Formu oturuma bağlı rastgele CSRF değeriyle gönderin. Aşağıdaki dosyada bootstrap.php, oturum yazısındaki ortak başlatma dosyasıdır.
<?php
require __DIR__ . '/bootstrap.php';
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$csrf = htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8');
?>
<form method="post" action="/cikis.php">
<input type="hidden" name="csrf" value="<?= $csrf ?>">
<button type="submit">Bu cihazdan çıkış yap</button>
</form>
Denetleyicinin ilk işi yöntemi ve CSRF değerini kontrol etmektir. CSRF değeri eksikse iki boş metni birbirine eşitleyip geçerli saymayın. Dizi olarak gönderilen form alanlarını da reddedin.
<?php
require __DIR__ . '/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
exit;
}
$sent = $_POST['csrf'] ?? null;
$known = $_SESSION['csrf'] ?? null;
if (!is_string($sent) || !is_string($known) || $known === ''
|| !hash_equals($known, $sent)) {
http_response_code(403);
exit('Gecersiz istek.');
}
// Buradan sonra cihaz tokenini iptal edip oturumu kapatin.
Kalıcı girişi kaldırmadan oturumu kapatmayın
“Beni hatırla” açıkken sadece session_destroy() çağırırsanız, sonraki istek kalıcı çerezle yeniden giriş yapabilir. Önce bu cihazın tokenını sunucudan kaldırın. Silme sorgusunu hem selector hem oturumdaki kullanıcı kimliğiyle sınırlandırın. Böylece formdan gelen bir kullanıcı kimliğine güvenmemiş olursunuz.
<?php
function revokeThisDevice(PDO $pdo, int $userId, string $cookie): void
{
if (!preg_match('/\A([a-f0-9]{24}):[a-f0-9]{64}\z/D', $cookie, $m)) {
return;
}
$stmt = $pdo->prepare(
'DELETE FROM remember_tokens WHERE selector = ? AND user_id = ?'
);
$stmt->execute([$m[1], $userId]);
}
Bu fonksiyon CSRF kontrolünden sonra çağrılır; $userId doğrulanmış oturumdan, çerez metni $_COOKIE['__Host-remember'] değerinden alınır. PDO hatası oluşursa cihaz tokenının iptal edildiğini varsaymayın. Yerel oturumu yine kapatabilir, fakat “bütün erişimler iptal edildi” mesajı vermeden hatayı izlemeye almalısınız. Kritik uygulamalarda iptal işleminin başarısı kullanıcıya açıkça bildirilir.
Oturum verisini ve iki çerezi temizleyin
session_destroy() sunucudaki oturum verisini siler; $_SESSION dizisini ve tarayıcı çerezini kendi başına temizlemez. Oturum çerezini silerken mevcut Path ve Domain kapsamını koruyun. Farklı kapsamla silinen aynı adlı çerez, asıl kaydı tarayıcıda bırakabilir.
<?php
function clearCurrentSession(): void
{
$params = session_get_cookie_params();
$_SESSION = [];
if (!session_destroy()) {
throw new RuntimeException('Sunucudaki oturum silinemedi.');
}
$options = [
'expires' => time() - 3600,
'path' => $params['path'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'] ?: 'Lax',
];
if ($params['domain'] !== '') {
$options['domain'] = $params['domain'];
}
setcookie(session_name(), '', $options);
setcookie('__Host-remember', '', [
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
header('Cache-Control: no-store');
header('Location: /giris.php', true, 303);
}
Fonksiyon çıktı gönderilmeden çalışmalıdır. Çağıran denetleyici sonrasında exit; ile bitmelidir. Aynı istekte tekrar session_start() çağırıp yeni oturum oluşturan alt bilgi veya menü dosyalarını çalıştırmayın.
Tüm cihazlardan çıkış için oturum sürümü kullanın
Sunucuda sadece mevcut oturum kimliğini biliyorsanız diğer cihazların dosyalarını doğrudan bulamazsınız. Kullanıcı satırına auth_version INT UNSIGNED NOT NULL DEFAULT 1 alanı koymak bu ihtiyacı karşılayan basit bir yöntemdir. Başarılı girişte güncel değeri oturuma yazın. Korunan her istekte oturumdaki değer ile kullanıcı tablosundaki değeri karşılaştırın.
Aşağıdaki fonksiyon “tüm cihazlardan çıkış” işleminin veritabanı kısmıdır. Kullanıcı açıkça bu işlemi seçmeli, POST ve CSRF kontrolü geçmelidir. Parola yenileme gibi diğer hesap işlemlerinde de aynı kilit sırasını kullanın.
<?php
function revokeAllDevices(PDO $pdo, int $userId): void
{
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare(
'UPDATE users SET auth_version = auth_version + 1 WHERE id = ?'
);
$stmt->execute([$userId]);
if ($stmt->rowCount() !== 1) {
throw new RuntimeException('Hesap bulunamadi.');
}
$pdo->prepare('DELETE FROM remember_tokens WHERE user_id = ?')
->execute([$userId]);
$pdo->commit();
} catch (Throwable $error) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
Bu işlemden sonra mevcut cihazda da clearCurrentSession() çağırın. Diğer cihazlar bir sonraki korunan istekte aşağıdaki denetim sayesinde reddedilir. Bu denetim sadece HTML sayfalarında değil, dosya indirme, AJAX ve API uçlarında da bulunmalıdır.
<?php
function sessionIsCurrent(PDO $pdo, array $session): bool
{
if (!isset($session['user_id'], $session['auth_version'])) {
return false;
}
$stmt = $pdo->prepare(
'SELECT is_active, auth_version FROM users WHERE id = ?'
);
$stmt->execute([(int) $session['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user !== false
&& (int) $user['is_active'] === 1
&& (int) $user['auth_version'] === (int) $session['auth_version'];
}
Fonksiyon false dönerse korunan içeriği üretmeyin; HTML isteğinde girişe yönlendirin, API isteğinde uygun yetkisiz yanıtı verin. Kullanıcı durumunu önbelleğe alıyorsanız “tüm cihazlar” işlemi o önbelleği de geçersizleştirmelidir. Aksi hâlde iptal, önbellek süresi bitene kadar gecikir.
Bir istek kontrolü geçtikten hemen sonra başka cihazdan iptal yapılabilir. Oturum sürümü kontrolü başlamış işi sihirli biçimde geri almaz. Para transferi veya hesap adresi değiştirme gibi kritik işlemlerde yetkiyi işlem anında yeniden denetleyin. Kalıcı bağlantılarda, örneğin WebSocket üzerinde, ayrıca bağlantı kapatma veya periyodik yeniden doğrulama gerekir.
Geri tuşu ile gerçek erişimi ayırın
Çıkıştan sonra geri tuşuna basınca tarayıcı önceden gösterilmiş sayfanın görüntüsünü sunabilir. Bu görüntü tek başına oturumun açık olduğunu göstermez. Korunan sayfalarda da Cache-Control: no-store kullanın; yalnızca çıkış yanıtına eklemek yeterli değildir. Asıl kontrol, sayfayı yenilemek veya korunan bir API isteği yapmaktır.
Başvuru: PHP session_destroy ve OWASP oturum yönetimi.
İki tarayıcı profilinde aynı hesapla giriş yapın. “Bu cihazdan çıkış” ilk profili kapatmalı, ikinci profil çalışmaya devam etmelidir. Ardından tekrar giriş yapıp “tüm cihazlardan çıkış” işlemini deneyin: her iki profildeki yeni korunan istek reddedilmeli, kalıcı giriş çerezi oturumu yeniden açmamalıdır. Eksik CSRF ile gönderilen çıkış isteği ise 403 dönmeli ve geçerli oturumu kapatmamalıdır.