الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
How To Make It

اعمل مصادقة JWT بـ Access و Refresh Token في Node.js مع تدوير آمن

متوسط10 أغسطس 20266 دقائق قراءة
اعمل مصادقة JWT بـ Access و Refresh Token في Node.js مع تدوير آمن

يتطلب مستوى: متوسط — الدليل مناسب لمن يعرف أساسيات Node.js و Express ويريد تأمين تسجيل الدخول بطريقة احترافية. لو أنت مبتدئ تمامًا، فيه في الطريق مثال مبسّط يقرّب الفكرة قبل الكود.

مصادقة JWT بـ Access و Refresh Token في Node.js

في نهاية الدليل هيبقى عندك نظام دخول يخلّي المستخدم مسجّلًا أسبوعًا كاملًا، من غير ما تسيب توكن خطير على جهازه طول المدة. الفكرة: توكن وصول عمره 15 دقيقة، وتوكن تجديد عمره 7 أيام يتدوّر مع كل استخدام.

المشكلة باختصار

لو حطّيت توكن واحد عمره أسبوع، أي تسريب له يفتح حساب المستخدم أسبوعًا كاملًا. ولو خلّيته قصيرًا جدًا، المستخدم هيتطرد كل ربع ساعة. الحل مش توكن أطول ولا أقصر، الحل توكنين بمهمتين مختلفتين.

شاشة محرر أكواد JavaScript تعرض كود مصادقة في Node.js يمثل بناء نظام JWT بـ Access و Refresh Token

مثال يقرّب الفكرة ثم الشرح الدقيق

تخيّل فندقًا. ساعة الوصول بيدّيك بطاقة غرفة تفتح الباب، بس بتنتهي كل يوم. بدل ما ترجع الاستقبال كل يوم بجواز سفرك، عندك إيصال حجز في الخزنة يخلّي الاستقبال يجدّد البطاقة بسرعة. البطاقة اليومية هي توكن الوصول، وإيصال الحجز هو توكن التجديد.

علميًا: الـ JWT توكن موقّع رقميًا يحمل بيانات (مثل معرّف المستخدم ودوره) وتوقيعًا. السيرفر بيتحقق من التوقيع بمفتاحه السرّي بدون رجوع لقاعدة البيانات. توكن الوصول (Access) قصير ويُرسل مع كل طلب في ترويسة Authorization. توكن التجديد (Refresh) طويل ووظيفته الوحيدة: إصدار توكن وصول جديد لما القديم ينتهي.

الافتراض هنا: عندك API واحد يشتغل على HTTPS، ومخزن للتوكنات (PostgreSQL أو Redis). الأرقام في المقال مبنية على سيناريو ~50 ألف مستخدم نشِط يوميًا.

الخطوات

  1. جهّز المشروع. ركّب الحزم الأساسية.
Bash
mkdir jwt-auth && cd jwt-auth
npm init -y
npm install express jsonwebtoken bcryptjs cookie-parser
  1. ولّد مفتاحين سرّيين منفصلين. مفتاح للوصول ومفتاح للتجديد، عشان لو اتسرّب واحد ما ينهارش التاني.
Bash
echo "ACCESS_SECRET=$(openssl rand -hex 32)" >> .env
echo "REFRESH_SECRET=$(openssl rand -hex 32)" >> .env
  1. اكتب دوال توقيع التوكنات. الوصول 15 دقيقة، التجديد 7 أيام، وكل توكن تجديد له معرّف فريد (jti) هنستخدمه في التدوير.
JavaScript
const jwt = require('jsonwebtoken');

const signAccess = (user) =>
  jwt.sign({ sub: user.id, role: user.role }, process.env.ACCESS_SECRET, { expiresIn: '15m' });

const signRefresh = (user, jti) =>
  jwt.sign({ sub: user.id, jti }, process.env.REFRESH_SECRET, { expiresIn: '7d' });
  1. اعمل مسار تسجيل الدخول. تحقّق من كلمة السر بـ bcrypt، وخزّن سجلّ توكن التجديد، وابعت التجديد في كوكي httpOnly.
JavaScript
app.post('/login', async (req, res) => {
  const user = await findUserByEmail(req.body.email);
  if (!user || !(await bcrypt.compare(req.body.password, user.hash)))
    return res.status(401).json({ error: 'invalid credentials' });

  const jti = crypto.randomUUID();
  await saveRefreshToken({ jti, userId: user.id, valid: true }); // سجلّ العائلة
  res.cookie('rt', signRefresh(user, jti), {
    httpOnly: true, secure: true, sameSite: 'strict',
    path: '/refresh', maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.json({ accessToken: signAccess(user) });
});
  1. احمِ المسارات بوسيط تحقّق. يقرأ توكن الوصول من الترويسة ويتحقق من التوقيع في الذاكرة، بدون قاعدة بيانات.
JavaScript
function requireAuth(req, res, next) {
  const token = (req.headers.authorization || '').replace('Bearer ', '');
  try {
    req.user = jwt.verify(token, process.env.ACCESS_SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'token expired or invalid' });
  }
}
  1. اعمل مسار التجديد بالتدوير واكتشاف إعادة الاستخدام. ده قلب الأمان: كل توكن تجديد يُستخدم مرة واحدة، ولو اتعاد استخدامه، تُبطَل جلسات المستخدم كلها.
JavaScript
app.post('/refresh', async (req, res) => {
  const token = req.cookies.rt;
  if (!token) return res.status(401).json({ error: 'no refresh token' });

  let payload;
  try { payload = jwt.verify(token, process.env.REFRESH_SECRET); }
  catch { return res.status(401).json({ error: 'invalid refresh token' }); }

  const stored = await getRefreshToken(payload.jti);

  // توقيع سليم لكن التوكن مصروف = محاولة إعادة استخدام
  if (!stored || !stored.valid) {
    await revokeAllForUser(payload.sub); // اقتل العائلة كلها
    return res.status(401).json({ error: 'reuse detected, logged out everywhere' });
  }

  await invalidateRefreshToken(payload.jti);          // احرق القديم
  const newJti = crypto.randomUUID();
  await saveRefreshToken({ jti: newJti, userId: payload.sub, valid: true });

  const user = await findUserById(payload.sub);
  res.cookie('rt', signRefresh(user, newJti), {
    httpOnly: true, secure: true, sameSite: 'strict',
    path: '/refresh', maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.json({ accessToken: signAccess(user) });
});
تدفقات بيانات مشفرة على خلفية داكنة ترمز إلى تدوير الـ Refresh Token واكتشاف إعادة الاستخدام لحماية الجلسات

ليه ده مهم بالظبط: لو سُرق توكن تجديد، أول ما المستخدم الحقيقي (أو المهاجم) يجدّد، النسخة القديمة تبقى مصروفة. أول استخدام لاحق للنسخة المسروقة بيتحوّل لإنذار، فتُقفَل العائلة كلها. السرقة عمرها ما بتعيش أطول من دورة تجديد واحدة.

  1. اعمل مسار الخروج. يبطّل توكن التجديد الحالي ويمسح الكوكي.
JavaScript
app.post('/logout', async (req, res) => {
  if (req.cookies.rt) {
    try {
      const p = jwt.verify(req.cookies.rt, process.env.REFRESH_SECRET);
      await invalidateRefreshToken(p.jti);
    } catch {}
  }
  res.clearCookie('rt', { path: '/refresh' });
  res.json({ ok: true });
});

التحقق من أنه يعمل

شغّل السيرفر وجرّب التسلسل ده بـ curl. المهم الخطوة الأخيرة: إعادة استخدام كوكي قديم لازم تفشل وتطرد المستخدم.

Bash
# 1) تسجيل الدخول وحفظ الكوكي
curl -i -c cookies.txt -X POST localhost:3000/login \
  -H 'content-type: application/json' \
  -d '{"email":"a@b.com","password":"secret"}'

# 2) نداء مسار محمي بتوكن الوصول
curl localhost:3000/me -H "authorization: Bearer <ACCESS>"

# 3) تجديد (بيدوّر توكن التجديد)
curl -i -b cookies.txt -c cookies.txt -X POST localhost:3000/refresh

# 4) أعِد استخدام الكوكي القديم عمدًا -> لازم يرجّع 401 ويبطّل العائلة

المقايضات (Trade-offs)

  • توكن الوصول عديم الحالة. بتكسب تحقّقًا سريعًا (~0.3 مللي ثانية HMAC في الذاكرة) بدون ضربة قاعدة بيانات. في سيناريو 50 ألف مستخدم و~40 طلب لكل مستخدم يوميًا، ده يوفّر ~2 مليون استعلام جلسة كل يوم. بتخسر القدرة على الإلغاء الفوري: التوكن يفضل صالحًا حتى ينتهي (حد أقصى 15 دقيقة).
  • التجديد ذو حالة. بتكسب اكتشاف إعادة الاستخدام والإلغاء الحقيقي. بتخسر النقاء عديم الحالة، لأنك محتاج تخزّن سجلات التوكنات في Redis أو Postgres.
  • كوكي httpOnly للتجديد. بتكسب حماية من قراءة التوكن عبر XSS. بتخسر البساطة: لازم تضيف حماية CSRF، و sameSite=strict مع مسار مخصّص (path=/refresh) بيقلّلوا السطح.

متى لا تستخدم هذه الطريقة

لو تطبيقك موقع واحد يُرسَم من السيرفر (server-rendered) بسيرفر واحد، الجلسات التقليدية أبسط وأأمن افتراضيًا. لو محتاج طرد فوري وعالمي للمستخدم (حظر لحظي)، الجلسات على السيرفر أو قائمة منع (denylist) تُفحص كل طلب أنسب من الوصول عديم الحالة. ولو أداة داخلية صغيرة بعدد مستخدمين محدود، الحل ده زيادة عن اللزوم.

الخطوة التالية

افتح مسار /refresh عندك دلوقتي وأضِف سطر invalidateRefreshToken قبل إصدار التوكن الجديد، وسطر revokeAllForUser في فرع إعادة الاستخدام. بعدها شغّل خطوة التحقق رقم 4: لو الكوكي القديم لسه بيشتغل، يبقى التدوير مش متفعّل.

المصادر

  • RFC 7519 — JSON Web Token (JWT)
  • OWASP — JSON Web Token Cheat Sheet
  • OWASP — Session Management Cheat Sheet
  • Auth0 — Refresh Token Rotation
  • توثيق مكتبة jsonwebtoken على GitHub

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة