خزّن كلمات المرور بـ password_hash($password, PASSWORD_DEFAULT) وافحص تسجيل الدخول بـ password_verify($password, $hash)، التي تعيد true أو false. لا تخزّن كلمة المرور نفسها أبدًا، ولا تستخدم md5() أو sha1() لها أبدًا.
شغّله مرتين: التجزئة مختلفة في كل مرة، والنسختان تجتازان الفحص. وينطبق الأمر نفسه على كل تجزئة في هذه الصفحة، فلن تطابق مخرجاتك مخرجات أي شخص آخر.
تسجيل الدخول بـ password_verify
يفحص تسجيل الدخول كلمة المرور المرسلة مقابل التجزئة المحفوظة عند إنشاء الحساب. «تسجّل» هذه الكتلة مستخدمًا واحدًا ثم تعرض نموذج تسجيل دخول؛ جرّب كلمة المرور الصحيحة (hunter2)، ثم كلمة خاطئة.
رسالة الخطأ واحدة للبريد غير المعروف ولكلمة المرور الخاطئة. فالقول أيهما كان خاطئًا يخبر المهاجم بأي عناوين البريد لها حسابات. وبعد تسجيل دخول ناجح، يخزّن الموقع الحقيقي معرّف المستخدم في الجلسة، كما تبيّن صفحة الجلسات.
ما الذي داخل التجزئة
تجزئة bcrypt نص واحد يحمل كل ما تحتاجه password_verify(): الخوارزمية، والتكلفة، والملح، ثم التجزئة نفسها. وتقرأ password_get_info() الأجزاء الأولى.
لأن الملح مخزّن في التجزئة، لا تحتاج إلا إلى عمود واحد في قاعدة البيانات، ويحصل مستخدمان لهما كلمة المرور نفسها على تجزئتين مختلفتين. اجعل ذلك العمود VARCHAR(255): طول bcrypt هو 60 محرفًا، لكن يُسمح لـ PASSWORD_DEFAULT بالتغير إلى خوارزمية مخرجاتها أطول.
اختيار التكلفة
التكلفة هي مدى بطء كل تجزئة عمدًا، وكل خطوة إلى الأعلى تضاعف العمل. تستخدم PHP 8.3 التكلفة 10 لـ bcrypt (ورفعت PHP 8.4 القيمة الافتراضية إلى 12). يجب أن يستغرق تسجيل الدخول نحو 100 إلى 300 ميلي ثانية من التجزئة؛ قِس ذلك على خادمك:
تعتمد الأرقام على الجهاز، لكن كل سطر يجب أن يكون ضعف الذي قبله تقريبًا. والأعلى أكثر أمانًا من الكسر وأبطأ لكل تسجيل دخول، لذا اختر أعلى تكلفة تتحملها صفحة تسجيل الدخول لديك.
ترقية التجزئات القديمة بـ password_needs_rehash
عندما ترفع التكلفة أو تغيّر PHP قيمة PASSWORD_DEFAULT، تظل التجزئات الموجودة تعمل، وتخبرك password_needs_rehash() بأيها قديم. واللحظة الوحيدة التي تملك فيها كلمة المرور العادية هي تسجيل الدخول، لذا فهناك تجري الترقية:
تصبح البادئة $2y$10$ على شكل $2y$12$، وفي تسجيل الدخول التالي تعيد password_needs_rehash() القيمة false.
لا تستخدم md5 أو sha1 لكلمات المرور أبدًا
md5() وsha1() تجزئتان عامتان سريعتان دون ملح. والسرعة هي بالضبط الخطأ لكلمات المرور: يستطيع مهاجم معه جدول مسرّب تجربة مليارات التخمينات في الثانية على GPU، وتجزئة كلمة مرور شائعة دون ملح موجودة أصلًا في جداول بحث عامة. قارن السرعة:
في الوقت الذي تصنع فيه password_hash() تجزئة واحدة، تصنع md5 مئات الآلاف، فيصبح كل تخمين يجريه الكاسر أرخص بذلك القدر. ابحث عن 5f4dcc3b5aa765d61d8327deb882cf99 وستجد كلمة "password" فورًا.
إذا ورثت جدولًا من تجزئات md5، فلا تستطيع تحويلها مباشرة. غلّفها بدلًا من ذلك: خزّن password_hash($oldMd5) وتحقق بـ password_verify(md5($input), $stored)، ثم استبدل كلًّا منها بتجزئة عادية حين يسجل ذلك المستخدم دخوله في المرة التالية.
Argon2id وحد 72 بايتًا
يستخدم PASSWORD_ARGON2ID ذاكرة أكثر لكل تجزئة، وهذا يجعل الكسر بوحدات GPU أغلى بكثير. وهو متاح حين تُبنى PHP مع libargon2 (ومعظم حزم Linux كذلك)؛ تحقق بـ defined('PASSWORD_ARGON2ID').
يطبع السطر الأخير bool(true): كلمتا مرور مختلفتان تتطابقان لأن bcrypt يتجاهل كل ما بعد البايت 72. ونادرًا ما تبلغ كلمات المرور الحقيقية ذلك، لكن العبارات السرية باليابانية أو بخطوط متعددة البايتات أخرى تبلغ 72 بايتًا عند نحو 24 محرفًا. وإذا سمحت بكلمات مرور طويلة جدًا، فاستخدم Argon2id، الذي ليس له مثل هذا الحد. وتعمل password_verify() مع أي من الخوارزميتين، فيمكنك التبديل بـ password_needs_rehash() كما في الأعلى.
الأسئلة الشائعة
لماذا تعطي password_hash نتيجة مختلفة في كل مرة؟
يولّد كل استدعاء ملحًا عشوائيًا جديدًا ويخزّنه داخل التجزئة، فتعطي كلمة المرور نفسها نصًا مختلفًا في كل مرة. وهذا مقصود: تقرأ password_verify() الملح من التجزئة المخزنة، فتظل تعيد true لكلمة المرور الصحيحة.
كم يجب أن يكون طول عمود كلمة المرور لـ password_hash؟
تجزئة bcrypt من PASSWORD_DEFAULT طولها 60 محرفًا اليوم، لكن الخوارزمية الافتراضية قد تتغير في إصدار مستقبلي من PHP. استخدم VARCHAR(255) لتتسع تجزئة أطول دون ترحيل.
لماذا تعيد password_verify القيمة false دائمًا؟
الأسباب المعتادة تجزئة مقتطعة (يقطع عمود VARCHAR(50) التجزئة ذات 60 محرفًا)، أو تمرير password_hash() جديدة لشيء ما وسيطًا ثانيًا بدل التجزئة التي خزّنتها، أو المقارنة مع تجزئة صُنعت بـ md5()، أو مسافات زائدة في القيمة المخزنة. تحقق من أن strlen($hash) تساوي 60 ومرّر كلمة المرور العادية وسيطًا أول.
هل أستطيع فك قيمة password_hash في PHP؟
لا. التجزئة باتجاه واحد: لا توجد دالة تعيدها إلى كلمة المرور. ولفحص كلمة مرور تجزّئ المحاولة وتقارن، وهذا ما تفعله password_verify(). ولكلمة مرور منسية، أرسل رابط إعادة تعيين بدل كلمة المرور القديمة.
هل أستخدم PASSWORD_DEFAULT أم PASSWORD_BCRYPT أم PASSWORD_ARGON2ID؟
PASSWORD_DEFAULT هو الخيار المعتاد: إنه bcrypt اليوم وقد تنقله PHP إلى خوارزمية أقوى لاحقًا، وتتيح لك password_needs_rehash() اعتمادها تدريجيًا. وPASSWORD_ARGON2ID أقوى في مواجهة الكسر بوحدات GPU ولا بأس باستخدامه حين يتضمنه إصدار PHP لديك.