JWT Authentication — Authentication & Security
مصادقة JWT
JSON Web Token رمز موقّع يحمل معلومات (claims) عن المستخدم. الخادم يتحقق من صحته بالتوقيع دون استعلام قاعدة البيانات.
الأجزاء الثلاثة
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI0MiIsImV4cCI6MTc5MDAwMDAwMH0 . 4pcPyMD09olPSyXnrXCjTwXyr4BsezdI1AVTmud2fU4
Header Payload Signature- Header: الخوارزمية (HS256).
- Payload: البيانات (sub المعرّف، exp الانتهاء، iat وقت الإصدار، role...).
- Signature: توقيع للجزأين بسر الخادم. أي تعديل في الـ Payload يُبطل التوقيع.
⚠️ الـ Payload مُرمّز (Base64URL) وليس مشفراً: أي شخص يقرؤه:
const payload = "eyJzdWIiOiI0MiIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzkwMDAwMDAwfQ";
console.log(JSON.parse(Buffer.from(payload, "base64url").toString()));لا تضع فيه كلمات مرور أو بيانات حساسة.
الإصدار والتحقق
const jwt = require("jsonwebtoken");
const token = jwt.sign({ sub: "42", role: "user" }, process.env.JWT_SECRET, { expiresIn: "15m" });
const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ["HS256"] }); // يرمي إن كان مزوراً أو منتهياًقواعد التحقق:
- حدد algorithms صراحةً (تاريخياً استُغلت مكتبات تقبل alg: none أو تخلط الخوارزميات).
- السر: طويل وعشوائي (32 بايتاً فأكثر)، في متغيرات البيئة، وسر مختلف لـ Refresh Tokens.
- expiresIn قصير لرموز الوصول (5–15 دقيقة).
- تحقق أن المستخدم ما زال موجوداً ومفعّلاً عند الحاجة: الرمز يبقى صالحاً حتى لو حُذف الحساب.
أين يُخزَّن في المتصفح؟
| المكان | XSS | CSRF |
|---|---|---|
| localStorage | ❌ أي سكربت في صفحتك (أو مكتبة مخترقة) يقرؤه ويسرقه | ✅ لا يُرسل تلقائياً |
| ملف تعريف ارتباط HttpOnly; Secure; SameSite | ✅ لا يقرؤه JavaScript | ⚠️ يُرسل تلقائياً — SameSite يحمي في أغلب الحالات |
| الذاكرة فقط (متغير) | ✅ أفضل | يضيع عند التحديث ← يُجدد بـ Refresh Token في cookie |
التوصية الشائعة للويب: رمز وصول قصير في الذاكرة، و Refresh Token في cookie بخاصية HttpOnly. للتطبيقات الجوالة: التخزين الآمن للنظام (Keychain / Keystore، مثل expo-secure-store).
JWT ليس دائماً الخيار الأفضل
لتطبيق ويب واحد على نطاق واحد، الجلسات أبسط وتُلغى فوراً. JWT يلمع مع الجوال، والخدمات المتعددة، والـ APIs العامة.
تمرين
فك ترميز رمز JWT حقيقي من تطبيقك (أدوات المطور ← Network) دون معرفة السر — ماذا ترى؟ ثم عدّل حرفاً في الـ Payload وحاول التحقق منه بـ jwt.verify.
// npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
// ===== Generate Token =====
const generateToken = (user) => {
return jwt.sign(
{
userId: user._id,
email: user.email,
role: user.role
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
};
// ===== Register =====
app.post('/api/auth/register', async (req, res) => {
const { name, email, password } = req.body;
// التحقق من عدم وجود المستخدم
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'البريد مستخدم' });
}
// تشفير كلمة المرور
const hashedPassword = await bcrypt.hash(password, 12);
// إنشاء المستخدم
const user = await User.create({
name,
email,
password: hashedPassword
});
// إنشاء Token
const token = generateToken(user);
res.status(201).json({
success: true,
token,
user: { id: user._id, name: user.name, email: user.email }
});
});
// ===== Login =====
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// البحث عن المستخدم
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({ error: 'بيانات خاطئة' });
}
// التحقق من كلمة المرور
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ error: 'بيانات خاطئة' });
}
const token = generateToken(user);
res.json({
success: true,
token,
user: { id: user._id, name: user.name }
});
});
// ===== Auth Middleware =====
const protect = async (req, res, next) => {
// 1. Get token
let token;
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({ error: 'يجب تسجيل الدخول' });
}
// 2. Verify token
try {
const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }); // حدد الخوارزمية المقبولة
const user = await User.findById(decoded.userId).select('-password');
if (!user) return res.status(401).json({ error: 'المستخدم لم يعد موجوداً' });
req.user = user;
next();
} catch (err) {
return res.status(401).json({ error: 'Token غير صالح' });
}
};
// ===== Protected Route =====
app.get('/api/profile', protect, (req, res) => {
res.json({ user: req.user });
});اختبر فهمك