تخيل معايا… بنيت REST API شغال، وربطته بواجهة React شكلها جامد، وكل حاجة بتتحفظ فعليًا في MongoDB. حاسس إنك خلصت مشروع Full Stack حقيقي، صح؟ فيه مشكلة واحدة بس: أي حد فاتح المتصفح دلوقتي يقدر يمسح كل المستخدمين اللي عندك، أو يعدل بياناتهم، من غير ما يسجل دخول أصلاً! التطبيق بتاعك عريان تمامًا، ودلوقتي هنلبسه.
المحتويات
- أولاً: ليه الـ API بتاعك محتاج حماية دلوقتي مش بكرة
- ثانياً: Stateful ولا Stateless؟ الفرق اللي هيغير نظرتك
- ثالثاً: تسجيل حساب جديد بكلمة مرور مشفرة
- رابعاً: تسجيل الدخول وإصدار الـ JWT
- خامساً: حماية الـ Routes بـ Middleware
- سادساً: ربط التوثيق بواجهة React
- سابعاً: أخطاء شائعة هتقابلك
- ثامناً: خطوة قبل النشر
أولاً: ليه الـ API بتاعك محتاج حماية دلوقتي مش بكرة
جرب دلوقتي، افتح Postman أو curl وابعت طلب DELETE على أي مستخدم في الـ API اللي بنيناه في المقال اللي فات. هيتنفذ فورًا، من غير ما حد يسألك إنت مين أصلاً. الكود شغال تمام من الناحية التقنية، بس من ناحية الأمان هو مفتوح على مصراعيه لأي حد عنده الرابط. ده مش عيب في الكود، ده حاجة ناقصة عمدًا عشان نضيفها دلوقتي: مين المسموح له يعمل إيه.
الـ Authentication بيجاوب سؤال “إنت مين؟”، والـ Authorization بيجاوب سؤال “مسموحلك تعمل إيه؟”. في المقال ده هنركز على الأول: إزاي نعرف مين المستخدم أصلاً قبل ما نسمحله يعمل أي حاجة حساسة زي التعديل أو الحذف.
ثانياً: Stateful ولا Stateless؟ الفرق اللي هيغير نظرتك
في الطريقة القديمة والمعروفة (Session-based Authentication)، لما تسجل دخول، السيرفر بيعمل session ليك ويخزنها عنده (في الذاكرة أو في قاعدة بيانات)، وبيديك cookie فيها معرف بس. كل طلب بعد كده، السيرفر لازم يرجع يدور في مخزنه ويتأكد إن الـ session دي لسه موجودة وصالحة. ده اسمه Stateful: السيرفر عنده “حالة” (state) لازم يفتكرها عن كل مستخدم مسجل دخول.
تخيلها زي بوّاب نادي عنده دفتر مكتوب فيه أسامي كل اللي دخلوا الليلة. كل مرة حد يحاول يدخل قسم الـ VIP، البواب لازم يرجع يفتح الدفتر ويدور على اسمك فيه. لو الدفتر ضاع أو اتحرق، كل اللي جوه لازم يخرجوا ويثبتوا هويتهم تاني من الأول. ده بالظبط اللي بيحصل لو السيرفر بتاعك وقع أو عملت له restart والـ sessions متخزنة في الذاكرة بس: كل المستخدمين هيتسجلوا خروج فجأة.
الطريقة التانية، وهي اللي هنستخدمها، اسمها JWT (JSON Web Token)، وهي Stateless تمامًا. السيرفر مش بيخزن أي حاجة عن المستخدم بعد ما يسجل دخول! بدل كده، بيديله “توكن” موقّع رقميًا فيه كل المعلومات اللي محتاجها (زي معرف المستخدم)، والمستخدم هو اللي بيبعت التوكن ده مع كل طلب. السيرفر بس بيتحقق إن التوقيع صحيح، وخلاص، مش محتاج يدور في أي مكان. بنفس تشبيه النادي: بدل الدفتر، البواب بيدّيك سوار مميز بتوقيع خاص بيه لما تدخل. المرة الجاية، مش هيدور في دفتر، بس هيبص على السوار ويتأكد إن التوقيع بتاعه حقيقي. لو السوار معاه، يبقى معاه، مش محتاج يفتكرك إنت مين.
الكلمة الأهم هنا هي “الحالة” (state) نفسها: بدل ما تفضل محفوظة عند السيرفر، بقت محمولة مع المستخدم جوه التوكن ذاته. التوكن اتكون من 3 أجزاء (header.payload.signature)، الجزء اللي فيه بيانات المستخدم (payload) مقروء لأي حد يفتحه، مش مشفر، لكن التوقيع (signature) هو اللي بيضمن إن محدش قدر يعدل فيه من غير ما يعرف الـ JWT_SECRET. يعني السيرفر مش محتاج “يفتكر” حاجة، هو بس بيتأكد كل مرة إن التوقيع سليم، ولو سليم يبقى يقدر يثق في البيانات اللي جواه. لو عايز تشوف شكل التوكن ده بعينك وتفكه لأجزائه، جرب تحط أي توكن هتولده بعدين على jwt.io، هتشوف الأجزاء التلاتة ملونة وواضحة قدامك.
طيب ليه بالذات JWT مش الطريقة القديمة؟ دي أهم 4 أسباب حقيقية:
- متماشي مع مبدأ الـ REST نفسه: اللي اتكلمنا عنه في المقال اللي فات بيقول إن كل طلب لازم يكون مستقل بذاته، من غير ما السيرفر يحتاج يفتكر حاجة من الطلب اللي قبله. الـ JWT بيطبق المبدأ ده بالحرف، والـ sessions بتكسره من الأساس.
- التوسع (horizontal scaling) بيبقى مجاني تقريبًا: لو مشروعك كبر واحتجت أكتر من نسخة من السيرفر شغالة مع بعض (فاكر كلامنا في مقال الـ API؟)، مش هتحتاج تشارك نفس الذاكرة أو قاعدة الجلسات بين السيرفرات، كل نسخة تقدر تتحقق من التوكن لوحدها من غير ما تكلم أي نسخة تانية.
- مثالي لتطبيقات الميرن ستاك تحديدًا: الفرونت إند (React) والباك إند (Express) منفصلين تمامًا، وممكن حتى ييجي عليهم طلبات من تطبيق موبايل بعدين. الـ cookies التقليدية بتتعقد مع الـ CORS ومصادر مختلفة، لكن هيدر Authorization بيشتغل بنفس الشكل بالظبط أيًا كان مين اللي بعت الطلب.
- أخف على قاعدة البيانات: مش محتاج تعمل استعلام على قاعدة البيانات في كل طلب عشان تتأكد مين المستخدم، البيانات الأساسية (زي المعرف) موجودة جوه التوكن نفسه.
ومع ده، الـ JWT مش مثالي 100%: أكبر عيب فيه إنك مش تقدر “تلغي” توكن قبل ما ينتهي بسهولة، لأن السيرفر أصلاً مش بيخزنه عنده عشان يمسحه. لو سُرق توكن، هيفضل شغال لحد ما تنتهي صلاحيته (اللي هنحددها في السطور الجاية). عشان كده اختيار مدة الصلاحية المناسبة جزء أساسي من الأمان، مش تفصيلة نتجاهلها.
ثالثاً: تسجيل حساب جديد بكلمة مرور مشفرة
أول قاعدة ذهبية: ماتخزنش كلمة المرور كنص عادي في قاعدة البيانات أبدًا، حتى لو المشروع تعليمي. هنستخدم مكتبة bcrypt عشان تشفرها قبل ما تتخزن.
const bcrypt = require("bcrypt");
app.post("/register", async (req, res) => {
try {
const { name, email, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const result = await usersCollection.insertOne({
name,
email,
password: hashedPassword,
createdAt: new Date()
});
res.status(201).json({ id: result.insertedId });
} catch (err) {
res.status(500).json({ error: "حصل خطأ في إنشاء الحساب" });
}
});
الرقم 10 اللي بعت لـ bcrypt.hash اسمه salt rounds، وهو بيحدد قد إيه التشفير هيبقى بطيء ومعقد، وكل ما زودته كل ما كان أصعب على أي حد يكسره، بس هياخد وقت أطول في التنفيذ. الرقم 10 نقطة توازن كويسة لمعظم المشاريع.
رابعاً: تسجيل الدخول وإصدار الـ JWT
دلوقتي جاي دور اللحظة المهمة: المستخدم بيبعت الإيميل والباسورد، إحنا بنتأكد إنهم صح، وبعدين بنديله التوكن اللي هيثبت هويته في كل طلب جاي.
const jwt = require("jsonwebtoken");
app.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
const user = await usersCollection.findOne({ email });
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 = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: "7d"
});
res.json({ token });
} catch (err) {
res.status(500).json({ error: "حصل خطأ في تسجيل الدخول" });
}
});
لاحظ حاجتين مهمين: الأولى، بنرجع نفس رسالة الخطأ (“بيانات الدخول غلط”) سواء الإيميل غلط أو الباسورد غلط، ومتقولش للمستخدم “الإيميل مش موجود” لوحدها، عشان محدش يقدر يعرف إيميلات مسجلة فعلاً في نظامك عن طريق التجربة. الثانية، process.env.JWT_SECRET ده مفتاح سري لازم يكون طويل وعشوائي ومخزن في ملف .env، لو حد عرفه هيقدر يزور توكنز باسمك.
خامساً: حماية الـ Routes بـ Middleware
دلوقتي عندنا توكن، لازم نستخدمه فعليًا عشان نمنع أي حد من غيره يعدل أو يمسح. هنعمل middleware بيتحقق من التوكن قبل ما يسمح للطلب يكمل.
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: "لازم تسجل دخول الأول" });
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.id;
next();
} catch (err) {
res.status(401).json({ error: "التوكن غير صالح" });
}
}
app.put("/users/:id", authMiddleware, async (req, res) => {
// نفس كود التحديث اللي عملناه في المقال اللي فات
});
app.delete("/users/:id", authMiddleware, async (req, res) => {
// نفس كود الحذف اللي عملناه في المقال اللي فات
});
الفكرة السحرية هنا هي next(). الـ middleware ده بيتحط قبل الدالة الأساسية بتاعة الـ route مباشرة، ولو التوكن صح بينادي next() عشان يسمح للطلب يكمل لباقي الكود، ولو غلط بيرجع خطأ ويوقف الطلب تمامًا. أي route عايز تحميه، حط authMiddleware قبله وخلاص.
سادساً: ربط التوثيق بواجهة React
الواجهة محتاجة تحفظ التوكن بعد تسجيل الدخول، وتبعته مع كل طلب حساس زي الحذف أو التعديل.
async function login(email, password) {
const res = await fetch(`${API_URL}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (res.ok) {
localStorage.setItem("token", data.token);
}
}
async function deleteUser(id) {
const token = localStorage.getItem("token");
await fetch(`${API_URL}/users/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
fetchUsers();
}
لاحظ صيغة الهيدر: Bearer ${token}، كلمة Bearer دي اتفاقية معروفة معناها “حامل التوكن ده مخول”، والـ authMiddleware اللي كتبناه فوق بيتوقع الصيغة دي بالظبط لما بيعمل split(” “)[1].
سابعاً: أخطاء شائعة هتقابلك
- نسيان إرسال الهيدر Authorization مع الطلبات المحمية، فبترجع رسالة “لازم تسجل دخول الأول” حتى لو المستخدم مسجل فعلاً.
- تخزين كلمة المرور كنص عادي بدل ما تشفرها بـ bcrypt، ده خطأ أمني خطير مش مجرد تفصيلة.
- استخدام نفس JWT_SECRET ضعيف أو قابل للتخمين (زي “123456”)، لازم يكون طويل وعشوائي فعلاً.
- عدم التعامل مع انتهاء صلاحية التوكن في الواجهة، فالمستخدم بيفضل شايف رسائل خطأ غامضة بدل ما يتوجه لتسجيل الدخول تاني.
ثامناً: خطوة قبل النشر
ماتسيبش JWT_SECRET مكتوب صريح جوه الكود أبدًا، لازم يكون في ملف .env ومختلف تمامًا بين بيئة التطوير والإنتاج. وفكر كمان في مدة صلاحية التوكن (expiresIn): مدة قصيرة أأمن بس بتضطر المستخدم يسجل دخول أكتر، ومدة طويلة أريح بس لو التوكن اتسرق هيفضل صالح فترة أطول. مافيش رقم صح مطلق، بس أسبوع (زي اللي استخدمناه) نقطة بداية معقولة لمعظم المشاريع الصغيرة.
وبكده يبقى تطبيق الـ MERN بتاعك مش بس شغال، ده كمان محمي فعليًا زي أي تطبيق حقيقي في السوق. من فكرة، لـ API، لـ واجهة، لحماية كاملة، خطوة بخطوة. لو عندك سؤال عن أي جزء من السلسلة، قولّي في التعليقات.
اكتشاف المزيد من كود التطور
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.


