لماذا JWT؟
في الماضي، استخدمنا Sessions: الخادم يحفظ معرّف جلسة في الذاكرة ويرسل cookie للعميل. المشكلة: عندما يصبح لديك عدّة خوادم، تحتاج مشاركة هذه الجلسات (Redis).
JWT يحلّ المشكلة بأن يحمل البيانات داخل التوكن نفسه، مُوقَّعاً رقمياً. الخادم يتحقّق من التوقيع فقط — لا يحتاج تخزين الجلسات.
بنية JWT
JWT ثلاثة أجزاء مفصولة بنقطة:
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └─ Signature
│ └─────── Payload (بيانات)
└───────────── Header (معلومات)Header
{
"alg": "HS256",
"typ": "JWT"
}Payload
{
"sub": "123",
"email": "[email protected]",
"role": "user",
"iat": 1735603200,
"exp": 1735606800
}sub— subject (user id)iat— issued atexp— expires at
Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
SECRET_KEY
)مهم: أيّ شخص يستطيع قراءة payload — لكن لا أحد يستطيع تعديله بدون المفتاح السرّي.
التطبيق في Node.js
التثبيت
npm install jsonwebtoken bcrypt zod
npm install -D @types/jsonwebtoken @types/bcryptHelper لإنشاء والتحقّق
// 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
// 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 الحماية
// 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();
};
}الاستخدام
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) => {
// ...
});من جهة العميل
// تسجيل الدخول
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 جديد.
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. مفتاح سرّي قوي
# أنشئ مفتاحاً عشوائياً (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 في دالة:
// ✅
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 (بعد تسجيل الدخول). المفهومان مكمّلان.