كيف تنشئ نظام تسجيل دخول آمن في PHP

كيف تنشئ نظام تسجيل دخول آمن في PHP

عندما نبني أي موقع أو تطبيق ويب، فإن أول باب يدخل منه المستخدم هو باب تسجيل الدخول. وهذا الباب بالذات هو أكثر باب يتعرّض للمحاولات الخاطئة، والاختراق، وسوء الاستخدام. لذلك، إنشاء نظام تسجيل دخول آمن في PHP ليس مجرد كتابة نموذج HTML وربطه بقاعدة البيانات، بل هو سلسلة من القرارات الصحيحة التي تحمي المستخدم وتحميك أنت أيضًا كمطوّر.

في هذه المقالة سأشرح لك كيف تبني نظام تسجيل دخول آمن من الصفر، خطوة بخطوة، بطريقة عملية وواضحة، مع أمثلة PHP حقيقية يمكنك تعديلها واستخدامها في مشروعك. وسأحاول أن أكتبها بأسلوب قريب من المطوّر الذي يجلس وحده أمام الشاشة في الليل، يحاول أن يجعل التطبيق يعمل بشكل صحيح وآمن في نفس الوقت. لأن هذا بالضبط ما نفعله كثيرًا: نكتب الكود، ثم نعود لنسأل أنفسنا: هل هذا آمن فعلًا؟

الهدف هنا ليس فقط أن “يعمل” تسجيل الدخول، بل أن يكون محميًا من أشهر الثغرات مثل:

  • تخزين كلمات المرور بشكل خاطئ

  • هجمات SQL Injection

  • سرقة الجلسات Session Hijacking

  • هجمات التخمين Brute Force

  • مشاكل CSRF

  • إعادة استخدام كلمات المرور الضعيفة

  • تسريب بيانات المستخدم

سنبني نظامًا بسيطًا لكنه قوي، ويمكن تطويره لاحقًا لمشاريع أكبر.


الفكرة العامة لنظام تسجيل دخول آمن

نظام تسجيل الدخول الآمن عادة يتكون من عدة أجزاء:

  1. نموذج تسجيل الدخول.

  2. التحقق من البيانات المدخلة.

  3. البحث عن المستخدم في قاعدة البيانات باستخدام استعلامات آمنة.

  4. مقارنة كلمة المرور بطريقة صحيحة.

  5. إنشاء جلسة Session آمنة بعد نجاح الدخول.

  6. إضافة حماية ضد المحاولات المتكررة.

  7. تسجيل الخروج بشكل سليم.

  8. حماية الصفحات الداخلية.

  9. معالجة نسيان كلمة المرور بطريقة آمنة.

  10. إضافة CSRF token للنماذج الحساسة.

إذا أهملت جزءًا واحدًا فقط، فقد يصبح النظام كله هشًا. لذلك سنبني كل جزء بهدوء وبترتيب منطقي.


أولًا: تصميم قاعدة البيانات

قبل كتابة أي سطر PHP، نحتاج إلى جدول المستخدمين. المهم هنا ألا نخزن كلمة المرور كنص عادي أبدًا. هذا خطأ كبير جدًا، وما زال بعض المبتدئين يقعون فيه للأسف.

مثال جدول users

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

لماذا اخترنا VARCHAR(255) لكلمة المرور؟

لأن password_hash() في PHP قد ينتج سلسلة أطول من المتوقع حسب الخوارزمية المستخدمة في المستقبل، لذلك من الأفضل ترك مساحة كافية.

حقول إضافية مفيدة

يمكنك لاحقًا إضافة:

  • is_active

  • email_verified_at

  • last_login_at

  • failed_login_attempts

  • locked_until

هذه الحقول ستفيدك جدًا في تحسين الأمان وإدارة الحسابات.


ثانياً: الاتصال بقاعدة البيانات بشكل آمن

أفضل طريقة شائعة هي استخدام PDO، لأنه يدعم prepared statements بشكل ممتاز ويساعدك على تجنب SQL Injection.

ملف الاتصال db.php

<?php

$host = 'localhost';
$dbname = 'secure_auth';
$user = 'root';
$pass = '';

$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
    die("Database connection failed.");
}

لماذا هذا مهم؟

  • utf8mb4 يمنع مشاكل الترميز.

  • ERRMODE_EXCEPTION يجعل الأخطاء أوضح.

  • EMULATE_PREPARES = false يحسن الأمان مع MySQL في كثير من الحالات.

لا تعرض رسالة الخطأ الحقيقية للمستخدم في الإنتاج، لأن الرسائل التقنية قد تكشف معلومات غير مرغوبة.


ثالثاً: إنشاء صفحة التسجيل Register

قبل أن يتمكن المستخدم من تسجيل الدخول، يجب أن يكون لديه حساب. لذلك سنبني تسجيلًا آمنًا أيضًا.

نموذج التسجيل register.php

<?php
session_start();
require_once 'db.php';

$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';
    $confirm_password = $_POST['confirm_password'] ?? '';

    if ($name === '') {
        $errors[] = "الاسم مطلوب.";
    }

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = "البريد الإلكتروني غير صالح.";
    }

    if (strlen($password) < 8) {
        $errors[] = "كلمة المرور يجب أن تكون 8 أحرف على الأقل.";
    }

    if ($password !== $confirm_password) {
        $errors[] = "كلمتا المرور غير متطابقتين.";
    }

    if (empty($errors)) {
        $stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$email]);

        if ($stmt->fetch()) {
            $errors[] = "هذا البريد الإلكتروني مستخدم بالفعل.";
        } else {
            $hashedPassword = password_hash($password, PASSWORD_DEFAULT);

            $stmt = $pdo->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
            $stmt->execute([$name, $email, $hashedPassword]);

            header("Location: login.php?registered=1");
            exit;
        }
    }
}
?>

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>التسجيل</title>
</head>
<body>
    <h2>إنشاء حساب جديد</h2>

    <?php if (!empty($errors)): ?>
        <ul>
            <?php foreach ($errors as $error): ?>
                <li><?= htmlspecialchars($error) ?></li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>

    <form method="POST" action="">
        <label>الاسم</label>
        <input type="text" name="name" required>

        <label>البريد الإلكتروني</label>
        <input type="email" name="email" required>

        <label>كلمة المرور</label>
        <input type="password" name="password" required>

        <label>تأكيد كلمة المرور</label>
        <input type="password" name="confirm_password" required>

        <button type="submit">تسجيل</button>
    </form>
</body>
</html>

ملاحظات مهمة

  • استخدمنا password_hash() بدلًا من أي تشفير يدوي.

  • استخدمنا prepared statement.

  • استخدمنا htmlspecialchars() عند عرض الأخطاء لتفادي XSS.

  • لا تعتمد فقط على التحقق في الواجهة الأمامية، فالبحث الحقيقي يكون دائمًا في الخادم.


رابعاً: لماذا يجب استخدام password_hash و password_verify؟

هذه من أهم النقاط في المقال كله. كثيرون يخطئون هنا، ويظنون أن تشفير كلمة المرور بأي دالة مثل md5() أو sha1() يكفي. لكنه لا يكفي، بل هو غير مناسب تمامًا لتخزين كلمات المرور.

المثال الصحيح

$hash = password_hash($password, PASSWORD_DEFAULT);

if (password_verify($passwordInput, $hashFromDatabase)) {
    echo "كلمة المرور صحيحة";
}

لماذا password_hash() أفضل؟

  • يضيف salt تلقائيًا.

  • يستخدم خوارزميات مناسبة لتخزين كلمات المرور.

  • يمكنه التحديث مستقبلًا عندما تتغير المعايير.

لماذا لا نستخدم md5 أو sha1؟

لأنها سريعة جدًا، والسرعة هنا ليست ميزة. في كلمات المرور، السرعة تساعد المهاجم على تجربة ملايين الاحتمالات بسرعة أكبر.


خامساً: إنشاء صفحة تسجيل الدخول

الآن نأتي إلى قلب النظام: صفحة login.

ملف login.php

<?php
session_start();
require_once 'db.php';

$errors = [];

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

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = "البريد الإلكتروني غير صالح.";
    }

    if ($password === '') {
        $errors[] = "كلمة المرور مطلوبة.";
    }

    if (empty($errors)) {
        $stmt = $pdo->prepare("SELECT id, name, email, password FROM users WHERE email = ? LIMIT 1");
        $stmt->execute([$email]);
        $user = $stmt->fetch();

        if ($user && password_verify($password, $user['password'])) {
            session_regenerate_id(true);

            $_SESSION['user_id'] = $user['id'];
            $_SESSION['user_name'] = $user['name'];
            $_SESSION['user_email'] = $user['email'];

            header("Location: dashboard.php");
            exit;
        } else {
            $errors[] = "بيانات الدخول غير صحيحة.";
        }
    }
}
?>

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>تسجيل الدخول</title>
</head>
<body>
    <h2>تسجيل الدخول</h2>

    <?php if (isset($_GET['registered'])): ?>
        <p>تم إنشاء الحساب بنجاح. يمكنك تسجيل الدخول الآن.</p>
    <?php endif; ?>

    <?php if (!empty($errors)): ?>
        <ul>
            <?php foreach ($errors as $error): ?>
                <li><?= htmlspecialchars($error) ?></li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>

    <form method="POST" action="">
        <label>البريد الإلكتروني</label>
        <input type="email" name="email" required>

        <label>كلمة المرور</label>
        <input type="password" name="password" required>

        <button type="submit">دخول</button>
    </form>
</body>
</html>

ما الذي جعل هذا الكود أكثر أمانًا؟

  • لم نضع كلمة المرور في الاستعلام.

  • استخدمنا LIMIT 1.

  • استخدمنا session_regenerate_id(true) بعد نجاح الدخول.

  • اعتمدنا على password_verify().

خطأ شائع جدًا

بعض المطورين يكتبون شيئًا مثل:

SELECT * FROM users WHERE email = '$email' AND password = '$password'

هذا خطير جدًا لسببين:

  1. SQL Injection.

  2. تخزين/مقارنة كلمات المرور بشكل خاطئ.

لا تفعل هذا أبدًا.


سادساً: حماية النظام من SQL Injection

SQL Injection من أشهر الثغرات في تطبيقات الويب. الفكرة ببساطة هي أن المهاجم يرسل إدخالًا خبيثًا يحاول تغيير معنى الاستعلام.

مثال خطر

$email = $_POST['email'];
$query = "SELECT * FROM users WHERE email = '$email'";

إذا استخدمنا هذا الشكل، فقد يتمكن المهاجم من إدخال قيمة تغير الاستعلام كله.

الحل

استخدم prepared statements دائمًا:

$stmt = $pdo->prepare("SELECT id, name, password FROM users WHERE email = ?");
$stmt->execute([$email]);

أو:

$stmt = $pdo->prepare("SELECT id, name, password FROM users WHERE email = :email");
$stmt->execute(['email' => $email]);

هذا من أسهل وأهم التحسينات الأمنية التي يمكنك القيام بها فورًا.


سابعاً: كيف تدير الجلسات Session بشكل آمن؟

بعد تسجيل الدخول بنجاح، لا يكفي أن تضع user_id في session وتنتهي. هناك تفاصيل مهمة جدًا.

1. إعادة توليد session ID

session_regenerate_id(true);

هذا يمنع Session Fixation إلى حد كبير.

2. ضبط إعدادات الجلسة

يفضل قبل session_start() أن تضبط بعض الإعدادات:

ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);

لكن انتبه:

  • cookie_secure يجب أن يكون 1 فقط إذا كان الموقع يعمل عبر HTTPS.

  • httponly يمنع JavaScript من الوصول إلى الكوكيز.

  • use_strict_mode يساعد في منع بعض مشاكل الجلسات.

مثال أفضل لبدء الجلسة

<?php
ini_set('session.cookie_httponly', 1);
ini_set('session.use_strict_mode', 1);

if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
    ini_set('session.cookie_secure', 1);
}

session_start();

3. لا تخزن معلومات كثيرة في الجلسة

اكتفِ بما تحتاجه:

  • user_id

  • user_name

  • user_email

ولا تضع كلمة المرور أو أي بيانات حساسة غير ضرورية.


ثامناً: حماية الصفحات الداخلية

لا يكفي أن يكون هناك login. يجب أن تمنع الزائر غير المسجل من الوصول للصفحات المحمية.

ملف auth.php

<?php
session_start();

if (!isset($_SESSION['user_id'])) {
    header("Location: login.php");
    exit;
}

ثم في أي صفحة محمية:

<?php
require_once 'auth.php';
?>

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>لوحة التحكم</title>
</head>
<body>
    <h1>مرحبًا بك في لوحة التحكم</h1>
    <p>هذه الصفحة محمية ولا يمكن الوصول إليها بدون تسجيل دخول.</p>
</body>
</html>

هذا بسيط، لكنه أساسي.


تاسعاً: تسجيل الخروج بشكل صحيح

تسجيل الخروج يجب أن ينهي الجلسة تمامًا.

ملف logout.php

<?php
session_start();

$_SESSION = [];

if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 3600,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}

session_destroy();

header("Location: login.php");
exit;

لماذا هذه الخطوات؟

  • نفرغ $_SESSION

  • نحذف الكوكيز

  • ندمر الجلسة نفسها

  • نعيد المستخدم لصفحة الدخول

لا تكتفِ فقط بـ session_destroy() وحدها، لأن تنظيف الكوكيز مهم أيضًا.


عاشراً: حماية النموذج من CSRF

CSRF يعني أن يقوم موقع آخر بإرسال طلب باسم المستخدم دون علمه. هذا الخطر يظهر في النماذج الحساسة مثل تسجيل الدخول، وتغيير كلمة المرور، وتسجيل الخروج أحيانًا.

إنشاء CSRF token

في بداية الصفحة:

session_start();

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

إضافة التوكن داخل النموذج

<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">

التحقق من التوكن عند الإرسال

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';

    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        die("Invalid CSRF token");
    }

    // استكمال التحقق من البيانات
}

لماذا hash_equals()؟

لأنه يقارن بطريقة أكثر أمانًا من المقارنة العادية، ويقلل المخاطر المتعلقة بتوقيت المقارنة.


الحادي عشر: الحد من محاولات تسجيل الدخول Brute Force Protection

لو تركت صفحة الدخول بدون حماية، قد يحاول المهاجم تجربة آلاف كلمات المرور في وقت قصير. لذلك يجب أن تضع حدًا للمحاولات.

فكرة بسيطة

  • إذا فشل المستخدم أكثر من 5 مرات خلال فترة معينة، أوقفه مؤقتًا.

  • يمكنك ربط ذلك بالـ IP أو بالبريد الإلكتروني أو كليهما.

إضافة حقول في قاعدة البيانات

ALTER TABLE users
ADD failed_attempts INT DEFAULT 0,
ADD locked_until DATETIME NULL;

منطق التحقق

$stmt = $pdo->prepare("SELECT id, name, email, password, failed_attempts, locked_until FROM users WHERE email = ? LIMIT 1");
$stmt->execute([$email]);
$user = $stmt->fetch();

if ($user) {
    if (!empty($user['locked_until']) && strtotime($user['locked_until']) > time()) {
        $errors[] = "الحساب مقفل مؤقتًا. حاول لاحقًا.";
    } elseif (password_verify($password, $user['password'])) {
        $pdo->prepare("UPDATE users SET failed_attempts = 0, locked_until = NULL WHERE id = ?")->execute([$user['id']]);

        session_regenerate_id(true);
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['user_name'] = $user['name'];

        header("Location: dashboard.php");
        exit;
    } else {
        $failed = $user['failed_attempts'] + 1;

        if ($failed >= 5) {
            $lockedUntil = date('Y-m-d H:i:s', strtotime('+15 minutes'));

            $stmt = $pdo->prepare("UPDATE users SET failed_attempts = ?, locked_until = ? WHERE id = ?");
            $stmt->execute([$failed, $lockedUntil, $user['id']]);

            $errors[] = "تم قفل الحساب مؤقتًا بسبب محاولات كثيرة.";
        } else {
            $stmt = $pdo->prepare("UPDATE users SET failed_attempts = ? WHERE id = ?");
            $stmt->execute([$failed, $user['id']]);

            $errors[] = "بيانات الدخول غير صحيحة.";
        }
    }
} else {
    $errors[] = "بيانات الدخول غير صحيحة.";
}

ملاحظة مهمة

لا تكتب رسالة توضح هل البريد الإلكتروني موجود أم لا بشكل واضح جدًا. من الأفضل أن تكون الرسالة عامة، حتى لا تساعد المهاجم على معرفة الحسابات الصحيحة.


الثاني عشر: سياسة كلمات المرور القوية

نظام تسجيل الدخول الآمن لا يعتمد فقط على الكود، بل أيضًا على سياسة كلمات المرور.

ما الذي نريده؟

  • طول مناسب، مثل 8 أو 12 حرفًا على الأقل.

  • مزيج من أحرف كبيرة وصغيرة وأرقام ورموز.

  • منع كلمات المرور الشائعة جدًا.

  • تشجيع عبارة مرور phrase passphrase قوية بدل كلمة قصيرة.

مثال بسيط للتحقق

function validatePasswordStrength($password) {
    if (strlen($password) < 8) {
        return "كلمة المرور قصيرة جدًا.";
    }

    if (!preg_match('/[A-Z]/', $password)) {
        return "يجب أن تحتوي كلمة المرور على حرف كبير.";
    }

    if (!preg_match('/[a-z]/', $password)) {
        return "يجب أن تحتوي كلمة المرور على حرف صغير.";
    }

    if (!preg_match('/[0-9]/', $password)) {
        return "يجب أن تحتوي كلمة المرور على رقم.";
    }

    return true;
}

هل هذا كافٍ؟

ليس دائمًا. أحيانًا تكون كلمة المرور طويلة لكنها ضعيفة مثل:
Password123!

هي تبدو قوية، لكنها شائعة جدًا. لذلك من الأفضل الجمع بين الطول والمعنى العشوائي.


الثالث عشر: صفحة نسيان كلمة المرور Forgot Password

هذه الصفحة غالبًا تُهمَل، لكنها من أكثر الأجزاء حساسية.

الفكرة الصحيحة

  1. المستخدم يطلب إعادة تعيين كلمة المرور.

  2. ترسل له رابطًا فيه token عشوائي.

  3. تحفظ token في قاعدة البيانات مع وقت انتهاء.

  4. عندما يفتح الرابط، يمكنه تعيين كلمة مرور جديدة.

  5. بعد التغيير، تحذف token القديم.

جدول مخصص

CREATE TABLE password_resets (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(150) NOT NULL,
    token VARCHAR(255) NOT NULL,
    expires_at DATETIME NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

إنشاء token

$token = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 minutes'));

حفظ الطلب

$stmt = $pdo->prepare("INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$email, $token, $expiresAt]);

ملاحظة أمنية مهمة

لا ترسل كلمة المرور الجديدة بالبريد. أرسل فقط رابط إعادة التعيين. ويفضل أن يكون الرابط لمرة واحدة.


الرابع عشر: إعادة تعيين كلمة المرور بأمان

التحقق من token

$stmt = $pdo->prepare("SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW() LIMIT 1");
$stmt->execute([$token]);
$reset = $stmt->fetch();

if (!$reset) {
    die("الرابط غير صالح أو منتهي الصلاحية.");
}

تحديث كلمة المرور

$newPasswordHash = password_hash($newPassword, PASSWORD_DEFAULT);

$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE email = ?");
$stmt->execute([$newPasswordHash, $reset['email']]);

$stmt = $pdo->prepare("DELETE FROM password_resets WHERE email = ?");
$stmt->execute([$reset['email']]);

لماذا نحذف token القديم؟

حتى لا يُستخدم مرة ثانية.


الخامس عشر: منع XSS عند عرض البيانات

الهجمات لا تأتي من قاعدة البيانات فقط. أحيانًا الخطر يأتي من عرض البيانات بشكل غير آمن.

مثال خطر

echo $_POST['name'];

لو كتب المستخدم نصًا خبيثًا يحتوي على JavaScript، فقد يُنفذ في المتصفح.

الحل

echo htmlspecialchars($_POST['name'], ENT_QUOTES, 'UTF-8');

قاعدة ذهبية

أي بيانات تأتي من المستخدم، اعتبرها غير آمنة حتى تثبت العكس.


السادس عشر: استخدام HTTPS

من دون HTTPS، يمكن اعتراض البيانات في الطريق بين المستخدم والخادم. وهذا خطر كبير خصوصًا في كلمات المرور والجلسات.

ما الذي يجب فعله؟

  • فعّل شهادة SSL/TLS.

  • اجعل الموقع يوجه تلقائيًا إلى HTTPS.

  • اجعل الكوكيز تحمل خاصية Secure.

مثال إعادة التوجيه في .htaccess

RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

هذا المثال مخصص لـ Apache.


السابع عشر: ترويسات أمان مهمة

يمكنك إضافة بعض ترويسات الأمان لتقوية التطبيق.

مثال

header("X-Frame-Options: DENY");
header("X-Content-Type-Options: nosniff");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'");

ماذا تفعل هذه الترويسات؟

  • X-Frame-Options: DENY يمنع تضمين الصفحة داخل iframe.

  • nosniff يقلل من بعض هجمات نوع المحتوى.

  • Referrer-Policy يتحكم في معلومات المرجع.

  • Content-Security-Policy يقلل من XSS.

قد تحتاج إلى تعديل CSP بحسب ملفات CSS وJS الخارجية لديك.


الثامن عشر: مثال كامل مبسط ومنظم

هنا مثال أكثر ترتيبًا لصفحة login آمنة نسبيًا:

<?php
ini_set('session.cookie_httponly', 1);
ini_set('session.use_strict_mode', 1);

if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
    ini_set('session.cookie_secure', 1);
}

session_start();
require_once 'db.php';

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $csrf = $_POST['csrf_token'] ?? '';

    if (!hash_equals($_SESSION['csrf_token'], $csrf)) {
        $errors[] = "فشل التحقق الأمني.";
    } else {
        $email = trim($_POST['email'] ?? '');
        $password = $_POST['password'] ?? '';

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors[] = "البريد الإلكتروني غير صالح.";
        }

        if ($password === '') {
            $errors[] = "كلمة المرور مطلوبة.";
        }

        if (empty($errors)) {
            $stmt = $pdo->prepare("SELECT id, name, email, password FROM users WHERE email = ? LIMIT 1");
            $stmt->execute([$email]);
            $user = $stmt->fetch();

            if ($user && password_verify($password, $user['password'])) {
                session_regenerate_id(true);

                $_SESSION['user_id'] = $user['id'];
                $_SESSION['user_name'] = $user['name'];
                $_SESSION['user_email'] = $user['email'];

                header("Location: dashboard.php");
                exit;
            } else {
                $errors[] = "بيانات الدخول غير صحيحة.";
            }
        }
    }
}
?>

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>تسجيل الدخول الآمن</title>
</head>
<body>
    <h2>تسجيل الدخول</h2>

    <?php if (!empty($errors)): ?>
        <div>
            <?php foreach ($errors as $error): ?>
                <p><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></p>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>

    <form method="POST" action="">
        <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">

        <div>
            <label>البريد الإلكتروني</label>
            <input type="email" name="email" required>
        </div>

        <div>
            <label>كلمة المرور</label>
            <input type="password" name="password" required>
        </div>

        <button type="submit">دخول</button>
    </form>
</body>
</html>

التاسع عشر: أفضل الممارسات التي يجب ألا تنساها

عندما تبني نظام تسجيل دخول، حاول أن تجعل هذه الأشياء عادة ثابتة لديك:

  • استخدم password_hash() و password_verify().

  • لا تخزن كلمات المرور كنص عادي.

  • استخدم prepared statements.

  • فعّل HTTPS.

  • أعد توليد session ID بعد تسجيل الدخول.

  • استخدم CSRF tokens.

  • امنع المحاولات المتكررة.

  • لا تعرض رسائل خطأ تقنية للمستخدم.

  • استخدم htmlspecialchars() عند الإخراج.

  • اجعل تسجيل الخروج يحذف الجلسة والكوكيز.

  • لا تمنح الثقة للبيانات القادمة من المتصفح.


العشرون: أخطاء شائعة يقع فيها المبتدئون

هذا الجزء مهم جدًا، لأن التعلم لا يكتمل فقط بمعرفة الصحيح، بل أيضًا بمعرفة الخطأ الذي يجب تجنبه.

1. استخدام md5 أو sha1 لكلمات المرور

هذا خطأ قديم لكنه ما زال موجودًا في كثير من الأكواد.

2. تخزين كلمة المرور في session

لا تفعل ذلك أبدًا.

3. بناء SQL يدويًا من مدخلات المستخدم

هذا يفتح الباب لـ SQL Injection.

4. نسيان session_regenerate_id(true)

خطأ صغير لكنه مهم جدًا.

5. استخدام رسائل خطأ تكشف معلومات كثيرة

مثل:

  • “هذا البريد غير موجود”

  • “كلمة المرور صحيحة لكن البريد خاطئ”
    هذه الرسائل تساعد المهاجم على الاستنتاج.

6. تجاهل CSRF

قد يبدو الأمر نظريًا في البداية، لكنه مهم جدًا في المشاريع الحقيقية.

7. الاعتماد على التحقق في الواجهة الأمامية فقط

التحقق الحقيقي يجب أن يكون في الخادم.


الحادي والعشرون: كيف تختبر النظام بعد بنائه؟

بعد كتابة الكود، لا تعتبر نفسك انتهيت. الاختبار جزء من الأمان.

اختبارات بسيطة

  • جرّب إدخال SQL injection في حقل البريد.

  • جرّب كلمات مرور قصيرة جدًا.

  • جرّب إدخال HTML أو JavaScript في الحقول.

  • جرّب تسجيل الدخول عدة مرات بشكل خاطئ.

  • جرّب الوصول إلى صفحة محمية دون تسجيل دخول.

  • جرّب تسجيل الخروج ثم الضغط على زر الرجوع في المتصفح.

ما الذي تتوقعه؟

  • يجب أن يفشل SQL injection.

  • يجب أن تُرفض البيانات غير الصالحة.

  • يجب أن تُحمي الجلسة.

  • يجب ألا تفتح الصفحة المحمية بدون session.


الثاني والعشرون: هل يجب استخدام أطر عمل مثل Laravel؟

في المشاريع الكبيرة، نعم، كثيرًا ما يكون استخدام إطار عمل مثل Laravel أفضل لأنه يوفر أدوات جاهزة للأمان مثل:

  • حماية CSRF

  • التحقق من البيانات

  • Auth scaffolding

  • Hashing

  • Middleware

  • Rate limiting

لكن فهمك لأساسيات تسجيل الدخول في PHP الخام مهم جدًا. لأنه حتى لو استخدمت Laravel، ستظل بحاجة إلى معرفة ما الذي يحدث تحت الغطاء.

الفهم الحقيقي يعطيك ثقة، ويجعل أخطاءك أقل، وقراراتك أفضل.


الثالث والعشرون: إضافة لمسة إنسانية للنظام

قد يبدو هذا غريبًا، لكن النظام الأمني ليس مجرد جدران وحواجز. هو أيضًا تجربة مستخدم.
المستخدم لا يحب أن يشعر أنه متهم في كل خطوة، ولا يحب الرسائل المربكة أو القاسية. كن واضحًا، مهذبًا، وحازمًا في نفس الوقت.

بدل أن تقول:
“Error: Invalid auth token.”

يمكنك أن تقول:
“حدث خطأ أمني بسيط. أعد المحاولة من فضلك.”

بدل أن تربك المستخدم برسالة فنية، خذ بيده بلطف. الأمن لا يعني الجفاف، والتقنية لا يجب أن تكون باردة طوال الوقت.


الخاتمة

إن بناء نظام تسجيل دخول آمن في PHP لا يعتمد على سطر واحد سحري، بل على مجموعة من العادات الصحيحة.
عندما تستخدم password_hash()، وpassword_verify()، وprepared statements، وCSRF tokens، وتدير الجلسات بشكل صحيح، فأنت لا تكتب فقط كودًا يعمل، بل تبني بابًا محترمًا وآمنًا يدخل منه المستخدم بثقة.

والأجمل من كل ذلك أنك كلما فهمت هذه الأساسيات جيدًا، ستتمكن من تطويرها لاحقًا إلى نظام كامل لإدارة المستخدمين، التحقق عبر البريد، صلاحيات الأدوار، تسجيل النشاط، وربما حتى المصادقة الثنائية 2FA.

خذ هذا المقال كقاعدة صلبة، وطبقه في مشروع حقيقي. لأن الأمن لا يتعلمه المطور من القراءة فقط، بل من التجربة، ومن الخطأ أحيانًا، ومن العودة لترتيب الكود بوعي أكبر.

#PHP secure login system #نظام تسجيل دخول آمن PHP #PHP authentication #PHP login tutorial #حماية تسجيل الدخول PHP #PHP sessions security #password hashing PHP