شرح JWT Authentication: كيف يعمل وكيف تستخدمه في Node.js
الأمن السيبراني

شرح JWT Authentication: كيف يعمل وكيف تستخدمه في Node.js

JSON Web Tokens (JWT) هي الطريقة الأشهر للمصادقة في APIs الحديثة. اشرح الكيفية، التطبيق، والأخطاء الشائعة.

م
مؤسس LahbabiGuide
4 دقائق قراءة
شارك:

لماذا JWT؟

في الماضي، استخدمنا Sessions: الخادم يحفظ معرّف جلسة في الذاكرة ويرسل cookie للعميل. المشكلة: عندما يصبح لديك عدّة خوادم، تحتاج مشاركة هذه الجلسات (Redis).

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

بنية JWT

JWT ثلاثة أجزاء مفصولة بنقطة:

xxxxx.yyyyy.zzzzz
│     │     │
│     │     └─ Signature
│     └─────── Payload (بيانات)
└───────────── Header (معلومات)
json
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

json
{
  "sub": "123",
  "email": "[email protected]",
  "role": "user",
  "iat": 1735603200,
  "exp": 1735606800
}
  • sub — subject (user id)
  • iat — issued at
  • exp — expires at

Signature

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  SECRET_KEY
)

مهم: أيّ شخص يستطيع قراءة payload — لكن لا أحد يستطيع تعديله بدون المفتاح السرّي.

التطبيق في Node.js

التثبيت

bash
npm install jsonwebtoken bcrypt zod
npm install -D @types/jsonwebtoken @types/bcrypt

Helper لإنشاء والتحقّق

js
// lib/auth.js
import jwt from "jsonwebtoken";

const SECRET = process.env.JWT_SECRET;
if (!SECRET) throw new Error("JWT_SECRET missing");

export function signToken(payload, expiresIn = "15m") {
  return jwt.sign(payload, SECRET, { expiresIn });
}

export function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET);
  } catch {
    return null;
  }
}

نموذج Login

js
// routes/auth.js
import express from "express";
import bcrypt from "bcrypt";
import { z } from "zod";
import { signToken } from "../lib/auth.js";
import { db } from "../db.js";

const router = express.Router();

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
});

router.post("/login", async (req, res) => {
  const parsed = LoginSchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: "Invalid" });

  const { email, password } = parsed.data;
  const user = await db.users.findOne({ email });
  if (!user) return res.status(401).json({ error: "خطأ في البيانات" });

  const ok = await bcrypt.compare(password, user.password);
  if (!ok) return res.status(401).json({ error: "خطأ في البيانات" });

  const accessToken = signToken({ sub: user.id, role: user.role });
  res.json({ accessToken, user: { id: user.id, email: user.email } });
});

Middleware الحماية

js
// middleware/auth.js
import { verifyToken } from "../lib/auth.js";

export function requireAuth(req, res, next) {
  const header = req.headers.authorization;
  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "مصادقة مطلوبة" });
  }
  const token = header.slice(7);
  const payload = verifyToken(token);
  if (!payload) return res.status(401).json({ error: "توكن غير صالح" });

  req.user = payload;
  next();
}

export function requireRole(role) {
  return (req, res, next) => {
    if (req.user?.role !== role) {
      return res.status(403).json({ error: "ممنوع" });
    }
    next();
  };
}

الاستخدام

js
import { requireAuth, requireRole } from "./middleware/auth.js";

app.get("/api/profile", requireAuth, (req, res) => {
  res.json({ userId: req.user.sub });
});

app.delete("/api/users/:id", requireAuth, requireRole("admin"), (req, res) => {
  // ...
});

من جهة العميل

js
// تسجيل الدخول
const { accessToken } = await fetch("/api/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email, password }),
}).then(r => r.json());

localStorage.setItem("token", accessToken);

// طلب محمي
fetch("/api/profile", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
إعلان

Access Token + Refresh Token

الـ access token قصير (15 دقيقة) لأنه يُرسَل كثيراً. الـ refresh token طويل (7-30 يوم) ويُستخدم للحصول على access جديد.

js
router.post("/refresh", async (req, res) => {
  const { refreshToken } = req.body;
  const payload = verifyToken(refreshToken);
  if (!payload) return res.status(401).json({ error: "منتهي" });

  // تحقّق من أن الـ refresh token لم يُلغَ
  const stored = await db.refreshTokens.findOne({ token: refreshToken });
  if (!stored) return res.status(401).json({ error: "ملغى" });

  const accessToken = signToken({ sub: payload.sub, role: payload.role });
  res.json({ accessToken });
});

أفضل الممارسات

1. مفتاح سرّي قوي

bash
# أنشئ مفتاحاً عشوائياً (256 بت)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

ضعه في .env واستبدله دورياً.

2. لا تحفظ JWT في localStorage للبيانات الحسّاسة

localStorage عرضة لـ XSS. الأفضل:

  • HttpOnly cookie — المتصفّح يرسله تلقائياً، JavaScript لا يراه
  • Memory storage — يُفقد عند إغلاق التبويب

3. استخدم الأقل معلومات في Payload

JWT ليس مشفّراً، فقط موقّع. لا تضع:

  • كلمات سر
  • أرقام بطاقات
  • بيانات سرّية

4. اضبط exp دائماً

JWT بدون انتهاء = كارثة. 15 دقيقة للـ access، 7 أيام للـ refresh.

5. آلية الإلغاء (Revocation)

JWT عادةً ليس قابلاً للإلغاء. الحلّ:

  • جدول blacklist قصير
  • أو حفظ tokenVersion في المستخدم، زدها عند تغيير كلمة السر

أخطاء شائعة

jwt.verify يرمي استثناءً

استخدم try/catch أو wrap في دالة:

js
// ✅
const payload = verifyToken(token); // null عند الفشل
if (!payload) return res.status(401).end();

استخدام خوارزمية "none"

بعض التنفيذات القديمة تسمح بتوكنات بدون توقيع (alg: none). مكتبة jsonwebtoken لا تسمح افتراضياً — لا تعطّل هذا أبداً.

نسيان تجديد access token

الواجهة يجب أن تكتشف 401، تطلب refresh، ثم تُعيد الطلب الأصلي.

الأسئلة الشائعة

JWT vs Sessions؟

  • Sessions: أبسط للتطبيقات الصغيرة، الإلغاء فوري
  • JWT: لازم لـ SPAs و mobile apps ومعمارية microservices

هل أستخدم JWT مع Passport.js؟

Passport يدعم JWT كاستراتيجية (passport-jwt). مفيد إن كنت تستخدم Passport أصلاً.

ماذا عن OAuth؟

OAuth بروتوكول كامل لتسجيل الدخول عبر Google/Facebook. JWT يمكن استخدامه كجزء من OAuth (بعد تسجيل الدخول). المفهومان مكمّلان.

اقرأ أيضاً

مقالات ذات صلة