جزِّئ كلمات المرور بدالة مكلفة، واختر بين الجلسات وJWT عن وعي كامل، واحمِ مساراتك ببرمجية وسيطة.
افتح هذا الدرس في Kodokonإن تخزين كلمة مرور يعني الاستعداد لاختراق مستقبلي: صمِّم التجزئة لتصمد أمام سرقة قاعدة البيانات. فالتجزئة السريعة مثل SHA-256 يمكن كسرها بالقوة الغاشمة بمليارات المحاولات في الثانية على وحدة معالجة رسومات (GPU). أنت بحاجة إلى دالة مكلفة عمدًا - scrypt، التي يوفّرها node:crypto، أو bcrypt/argon2 - وإلى مِلح (salt) عشوائي لكل مستخدم، يجعل الجداول المحسوبة مسبقًا عديمة الفائدة. والمِلح ليس سرًا: فهو يُخزَّن كنص صريح بجانب التجزئة.
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) التي تستهلكها أطراف ثالثة أو للبِنى الموزّعة.
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)) لحماية مجموعة كاملة من المسارات بدلًا من مسار تلو الآخر.
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'));
}
}timingSafeEqual في التحقق من كلمة المرور؟