Kodokon kodokon.com

المصادقة: التجزئة، والجلسات مقابل JWT

جزِّئ كلمات المرور بدالة مكلفة، واختر بين الجلسات وJWT عن وعي كامل، واحمِ مساراتك ببرمجية وسيطة.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

إن تخزين كلمة مرور يعني الاستعداد لاختراق مستقبلي: صمِّم التجزئة لتصمد أمام سرقة قاعدة البيانات. فالتجزئة السريعة مثل SHA-256 يمكن كسرها بالقوة الغاشمة بمليارات المحاولات في الثانية على وحدة معالجة رسومات (GPU). أنت بحاجة إلى دالة مكلفة عمدًا - scrypt، التي يوفّرها node:crypto، أو bcrypt/argon2 - وإلى مِلح (salt) عشوائي لكل مستخدم، يجعل الجداول المحسوبة مسبقًا عديمة الفائدة. والمِلح ليس سرًا: فهو يُخزَّن كنص صريح بجانب التجزئة.

JAVASCRIPT
import {
  randomBytes,
  scryptSync,
  timingSafeEqual,
} from 'node:crypto';

export function hashPassword(password) {
  const salt = randomBytes(16).toString('hex');
  const hash = scryptSync(password, salt, 64)
    .toString('hex');
  return `${salt}:${hash}`;
}

export function verifyPassword(password, stored) {
  const [salt, hash] = stored.split(':');
  const candidate = scryptSync(password, salt, 64);
  const expected = Buffer.from(hash, 'hex');
  return timingSafeEqual(candidate, expected);
}
مِلح عشوائي، واشتقاق مكلف، ومقارنة بزمن ثابت.

نموذجان لإبقاء المستخدم مسجّل الدخول. الجلسة: مُعرِّف مبهم في كوكي من نوع httpOnly، مع حالة في جانب الخادم. إبطال فوري، وكوكي غير مرئي لـ JavaScript، لكن مخزنًا مشتركًا (Redis) يصبح ضروريًا حالما تضيف نسخة ثانية. JWT: الحالة المُوقَّعة مضمَّنة في الرمز (token)، قابلة للتحقق دون أيّ مخزن - مثالية بين الخدمات - لكنها غير قابلة للإبطال قبل انتهاء صلاحيتها. والمقايضة الصادقة: الجلسات لتطبيق ويب تقليدي، ورموز JWT قصيرة العمر لواجهات برمجة التطبيقات (API) التي تستهلكها أطراف ثالثة أو للبِنى الموزّعة.

JAVASCRIPT
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET;
if (!SECRET) {
  throw new Error('JWT_SECRET must be set');
}

export function signToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '15m' },
  );
}
سر مطلوب عند بدء التشغيل، ورموز بعمر قصير.

تعزل برمجية الحماية الوسيطة التحكم في الوصول: فهي تستخرج الرمز من ترويسة Authorization: Bearer، وتتحقق منه، وتُرفِق الهوية المفكوكة بـ req.user، أو تقطع الطريق بخطأ 401 يُوجَّه إلى برمجية الأخطاء الوسيطة من الدرس السابق. طبّقها على مستوى الموجِّه (router.use(requireAuth)) لحماية مجموعة كاملة من المسارات بدلًا من مسار تلو الآخر.

JAVASCRIPT
import jwt from 'jsonwebtoken';
import { HttpError } from './http-error.js';

const SECRET = process.env.JWT_SECRET;

export function requireAuth(req, res, next) {
  const header = req.headers.authorization ?? '';
  const [scheme, token] = header.split(' ');
  if (scheme !== 'Bearer' || !token) {
    return next(new HttpError(401, 'Missing token'));
  }
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    next(new HttpError(401, 'Invalid or expired token'));
  }
}
تحقّق، وأرفِق req.user، أو أطلق 401 - لا شيء آخر.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. لماذا لا يصلح SHA-256 وحده لتخزين كلمات المرور؟
    • لأن الخوارزمية مكسورة تشفيريًا
    • لأنها سريعة جدًا: فوحدة معالجة الرسومات (GPU) تختبر مليارات المرشحين في الثانية؛ أنت بحاجة إلى دالة بطيئة ومُملَّحة
    • لأنها تُنتِج تصادمات (collisions) على كلمات المرور القصيرة
    • لأن Node لا يوفّرها أصليًا
  2. أيّ آلية تتيح لك إبطال الوصول فورًا، قبل أيّ انتهاء صلاحية؟
    • JWT، لأنه يضمّن تاريخ انتهاء صلاحيته
    • الجلسة في جانب الخادم، لأنك تكتفي بحذف مدخلها من المخزن
    • كلاهما، بالتساوي
  3. ما الغرض من timingSafeEqual في التحقق من كلمة المرور؟
    • لتسريع مقارنة التجزئتين
    • للمقارنة بزمن ثابت حتى لا تكشف مدة الاستجابة عن عدد البايتات المتطابقة
    • لتحويل التجزئات الست عشرية إلى Buffers