البرمجة متوسط 13 دقيقة

كيفية إضافة مصادقة JWT لواجهة Node.js API

JWT (JSON Web Tokens) هي الطريقة الأكثر شيوعًا للمصادقة في REST APIs عديمة الحالة. يُصدر الخادم token موقّعًا عند تسجيل الدخول؛ يرسله العميل مع كل طلب لاحق؛ يتحقق الخادم من التوقيع — دون مخزن جلسات، ودون رحلة إلى قاعدة البيانات مع كل طلب.

يغطي هذا الدليل الدورة الكاملة: تجزئة كلمات المرور بـ bcrypt، إصدار tokens عند تسجيل الدخول، كتابة authMiddleware يتحقق من token ويربط معرّف المستخدم بالطلب، حماية المسارات، وفهم المفاضلات بين تخزين tokens في localStorage مقابل httpOnly cookies.

الخطوات

  1. 1

    ثبّت الحزم المطلوبة

    jsonwebtoken يوقّع JWTs ويتحقق منها. bcryptjs تنفيذ bcrypt بـ JavaScript خالص — أبطأ من الربط الأصلي بـ C++ لكن بدون تبعيات أصلية وأسهل في النشر.

    bash
    npm install jsonwebtoken bcryptjs
  2. 2

    أضف JWT_SECRET لبيئتك

    السر يُستخدم لتوقيع كل token. إذا تسرّب، يستطيع المهاجمون تزوير tokens لأي مستخدم. استخدم سلسلة عشوائية طويلة — على الأقل 32 بايت. لا تكتبها في الكود مطلقًا.

    bash
    # .env
    PORT=3000
    JWT_SECRET=replace-this-with-a-long-random-string-at-least-32-chars
    
    # أنشئ واحدة جيدة بـ:
    # node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
  3. 3

    أنشئ مسار التسجيل

    معالج التسجيل يتحقق من المدخلات، يفحص البريد المكرر، يجزّئ كلمة المرور بـ bcrypt، ويخزن المستخدم. لا تخزن كلمة مرور كنص صريح أبدًا. عامل التكلفة 12 في bcrypt افتراضي جيد — كافٍ لإبطاء القوة الغاشمة دون إبطاء خادمك.

    javascript
    // src/controllers/authController.js
    const bcrypt = require("bcryptjs")
    const jwt = require("jsonwebtoken")
    
    // مخزن في الذاكرة — استبدله بقاعدة بيانات في الإنتاج
    const users = []
    let nextId = 1
    
    exports.signup = async (req, res) => {
      const { name, email, password } = req.body
      if (!name || !email || !password) {
        return res.status(400).json({ error: "name, email and password are required" })
      }
    
      if (users.find((u) => u.email === email)) {
        return res.status(409).json({ error: "Email already registered" })
      }
    
      const passwordHash = await bcrypt.hash(password, 12)
      const user = { id: nextId++, name, email, passwordHash }
      users.push(user)
    
      res.status(201).json({ message: "Account created", id: user.id })
    }
    
    module.exports.users = users
  4. 4

    أنشئ مسار تسجيل الدخول

    معالج تسجيل الدخول يجد المستخدم بالبريد الإلكتروني، يقارن كلمة المرور المُرسلة بالـ hash المخزّن عبر bcrypt.compare، ويُصدر JWT موقّعًا إذا كانت صحيحة. اضبط انتهاء صلاحية قصيرًا لـ access tokens: من '15m' إلى '1d' معتاد. يجب أن يحتوي payload على ما تحتاجه فقط في كل طلب (معرّف المستخدم يكفي عادةً).

    javascript
    // أضف إلى src/controllers/authController.js
    exports.login = async (req, res) => {
      const { email, password } = req.body
      if (!email || !password) {
        return res.status(400).json({ error: "email and password are required" })
      }
    
      const user = users.find((u) => u.email === email)
      if (!user) {
        // نفس الرسالة لمستخدم غير موجود وكلمة مرور خاطئة — لمنع تعداد المستخدمين
        return res.status(401).json({ error: "Invalid credentials" })
      }
    
      const passwordMatch = await bcrypt.compare(password, user.passwordHash)
      if (!passwordMatch) {
        return res.status(401).json({ error: "Invalid credentials" })
      }
    
      const token = jwt.sign(
        { id: user.id },
        process.env.JWT_SECRET,
        { expiresIn: "7d" }
      )
    
      res.json({ token })
    }
  5. 5

    اكتب auth middleware

    الـ middleware يقرأ ترويسة Authorization، يحذف بادئة Bearer ، يتحقق من token بالسر، ويربط payload المُفكَّك بـ req.userId. إذا فشل التحقق لأي سبب — منتهي الصلاحية، معبوث به، مفقود — يُعيد 401 فورًا.

    javascript
    // src/middleware/auth.js
    const jwt = require("jsonwebtoken")
    
    module.exports = function authMiddleware(req, res, next) {
      const authHeader = req.headers["authorization"]
      if (!authHeader || !authHeader.startsWith("Bearer ")) {
        return res.status(401).json({ error: "Missing or malformed Authorization header" })
      }
    
      const token = authHeader.slice(7) // أزل "Bearer "
    
      try {
        const payload = jwt.verify(token, process.env.JWT_SECRET)
        req.userId = payload.id
        next()
      } catch (err) {
        const message = err.name === "TokenExpiredError" ? "Token expired" : "Invalid token"
        return res.status(401).json({ error: message })
      }
    }
  6. 6

    اربط مسارات المصادقة واحمِ نقاط النهاية

    سجّل مسارات التسجيل وتسجيل الدخول — هذه عامة. احمِ أي مسار يتطلب مصادقة بتركيب الـ middleware قبل المعالج، إما بشكل مباشر لكل مسار أو كـ middleware على مستوى الـ router لمجموعة كاملة.

    javascript
    // src/routes/auth.js
    const express = require("express")
    const router = express.Router()
    const authController = require("../controllers/authController")
    
    router.post("/signup", authController.signup)
    router.post("/login", authController.login)
    
    module.exports = router
    
    // src/routes/users.js — احمِ الـ router بالكامل
    const authMiddleware = require("../middleware/auth")
    const router = express.Router()
    
    router.use(authMiddleware) // جميع مسارات /api/users تتطلب token صالح
    
    router.get("/", usersController.getAll)
    router.get("/:id", usersController.getOne)
    // ...
    
    // src/app.js
    app.use("/api/auth", authRouter)   // عام
    app.use("/api/users", usersRouter) // محمي عبر middleware على مستوى الـ router
  7. 7

    اختبر سير المصادقة الكامل

    شغّل الخادم وامشِ خلال الدورة الكاملة: التسجيل، تسجيل الدخول، استخدام token، محاولة token منتهي الصلاحية أو معبوث به.

    bash
    # 1. التسجيل
    curl -X POST http://localhost:3000/api/auth/signup \
      -H "Content-Type: application/json" \
      -d '{"name":"Alice","email":"alice@example.com","password":"secret123"}'
    
    # 2. تسجيل الدخول — انسخ token من الاستجابة
    curl -X POST http://localhost:3000/api/auth/login \
      -H "Content-Type: application/json" \
      -d '{"email":"alice@example.com","password":"secret123"}'
    
    # 3. الوصول لمسار محمي باستخدام token
    TOKEN="eyJhbGci..."
    curl http://localhost:3000/api/users \
      -H "Authorization: Bearer $TOKEN"
    
    # 4. جرّب بدون token — توقع 401
    curl http://localhost:3000/api/users
  8. 8

    افهم مفاضلات تخزين token

    مكان تخزين JWT على العميل مهم للأمان:

    • localStorage / sessionStorage — سهل الوصول من JavaScript، لكن عرضة لـ XSS. يستطيع سكريبت مُحقون سرقة token. مقبول فقط إذا كان API والواجهة على نفس الأصل ولديك CSP صارم.
    • httpOnly cookie — JavaScript لا يستطيع قراءته. محصّن ضد سرقة token عبر XSS. يُرسل Cookie تلقائيًا مع كل طلب. يتطلب حماية CSRF (استخدم SameSite=Strict أو Lax). هذا الافتراضي الأفضل لعملاء المتصفح.

    إذا كان API مستهلَكًا من تطبيق موبايل أو خادم آخر (لا متصفح)، فـ localStorage أو التخزين في الذاكرة مقبول — لا يوجد DOM، إذن XSS غير ذي صلة.

نصائح ومحاذير

  • أعد نفس رسالة الخطأ ("Invalid credentials") لكلمة المرور الخاطئة والبريد غير المعروف. رسائل مختلفة تتيح للمهاجمين تعداد البريد المسجّل.
  • يجب أن تكون access tokens قصيرة الصلاحية (15 دقيقة إلى يوم واحد). استخدم refresh token منفصل، طويل الصلاحية في httpOnly cookie لإصدار access tokens جديدة دون إجبار المستخدم على إعادة الدخول.
  • ضع الحد الأدنى من البيانات في JWT payload (معرّف المستخدم، ربما الدور). كل بايت يُضاف لكل طلب. تجنب تخزين البريد الإلكتروني أو الاسم — اجلبهما من قاعدة البيانات عند الحاجة.
  • خزّن `JWT_SECRET` في مدير أسرار (AWS Secrets Manager أو Doppler أو غيره) في الإنتاج. تدويره يُلغي جميع tokens الموجودة — خطّط لذلك مسبقًا.

خاتمة

مصادقة JWT مع bcrypt وjsonwebtoken تضيف أقل من 100 سطر لـ Express API. القرارات الحاسمة تشغيلية: أبقِ السر خارج الكود المصدري، استخدم انتهاء صلاحية قصيرًا مع استراتيجية refresh، واختر httpOnly cookies على localStorage لعملاء المتصفح. كل شيء آخر كود متكرر يمكن التنبؤ به بمجرد فهم دور كل جزء.

#Node.js #JWT #Auth
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.