تعد مصادقة JWT (JSON Web Token) هي المعيار لتأمين REST APIs في تطبيقات Node.js. في عام 2026، أصبحت أنماط التنفيذ راسخة – ولكن العديد من البرامج التعليمية تتخطى تفاصيل الأمان المهمة مثل تحديث تحديث الرمز المميز، والإدارة السرية المناسبة، وإبطال الرمز المميز. يغطي هذا الدليل التنفيذ الكامل والجاهز للإنتاج.
📋 Table of Contents
كيف تعمل مصادقة JWT
- يقوم المستخدم بتسجيل الدخول باستخدام بيانات الاعتماد → يقوم الخادم بالتحقق من صحتها
- يصدر الخادم رمزين مميزين:رمز الوصول (قصيرة الأجل، 15 دقيقة) ورمز التحديث (طويلة العمر 7 أيام)
- يرسل العميل رمز الوصول في رأس التفويض لكل طلب
- عند انتهاء صلاحية رمز الوصول، يرسل العميل رمز التحديث للحصول على رمز وصول جديد
- عند تسجيل الخروج، يتم إبطال رمز التحديث من جانب الخادم
الإعداد
npm install express jsonwebtoken bcryptjs cookie-parser ioredis
npm install -D @types/jsonwebtoken @types/bcryptjs
خدمة الرمز
// services/tokenService.js
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// CRITICAL: Use different secrets for access and refresh tokens
function generateAccessToken(payload) {
return jwt.sign(payload, ACCESS_SECRET, {
expiresIn: '15m',
issuer: 'myapp',
audience: 'myapp-users',
});
}
function generateRefreshToken(payload) {
return jwt.sign(payload, REFRESH_SECRET, {
expiresIn: '7d',
issuer: 'myapp',
audience: 'myapp-users',
jwtid: crypto.randomUUID(), // unique ID for invalidation
});
}
function verifyAccessToken(token) {
return jwt.verify(token, ACCESS_SECRET, {
issuer: 'myapp',
audience: 'myapp-users',
});
}
async function verifyRefreshToken(token) {
const payload = jwt.verify(token, REFRESH_SECRET, {
issuer: 'myapp',
audience: 'myapp-users',
});
// Check token hasn't been revoked
const isRevoked = await redis.get(`revoked:${payload.jti}`);
if (isRevoked) throw new Error('Token revoked');
return payload;
}
async function revokeRefreshToken(token) {
try {
const payload = jwt.decode(token);
if (payload?.jti && payload?.exp) {
const ttl = payload.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`revoked:${payload.jti}`, ttl, '1');
}
}
} catch (err) {
// Ignore decode errors on logout
}
}
module.exports = {
generateAccessToken, generateRefreshToken,
verifyAccessToken, verifyRefreshToken, revokeRefreshToken
};
طرق المصادقة
// routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const { generateAccessToken, generateRefreshToken,
verifyRefreshToken, revokeRefreshToken } = require('../services/tokenService');
const { findUserByEmail } = require('../db/users');
// POST /auth/login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.hashedPassword))) {
// Same error for missing user and wrong password (prevent user enumeration)
return res.status(401).json({ error: 'Invalid credentials' });
}
const payload = { sub: user.id, email: user.email, role: user.role };
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
// Store refresh token in httpOnly cookie (not accessible to JS)
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
});
res.json({ accessToken, user: { id: user.id, email: user.email, role: user.role } });
});
// POST /auth/refresh
router.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const payload = await verifyRefreshToken(refreshToken);
// Refresh token rotation: revoke old, issue new
await revokeRefreshToken(refreshToken);
const newRefreshToken = generateRefreshToken({
sub: payload.sub, email: payload.email, role: payload.role
});
const newAccessToken = generateAccessToken({
sub: payload.sub, email: payload.email, role: payload.role
});
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
} catch (err) {
res.clearCookie('refreshToken');
res.status(401).json({ error: 'Invalid or expired refresh token' });
}
});
// POST /auth/logout
router.post('/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) await revokeRefreshToken(refreshToken);
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
module.exports = router;
وسيطة المصادقة
// middleware/authenticate.js
const { verifyAccessToken } = require('../services/tokenService');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyAccessToken(token);
req.user = payload; // { sub, email, role, iat, exp }
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Access token expired', code: 'TOKEN_EXPIRED' });
}
res.status(401).json({ error: 'Invalid access token' });
}
}
// Role-based authorization
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user?.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
module.exports = { authenticate, authorize };
حماية الطرق
const { authenticate, authorize } = require('./middleware/authenticate');
// Public route
app.get('/health', (req, res) => res.json({ status: 'ok' }));
// Authenticated route
app.get('/api/profile', authenticate, (req, res) => {
res.json({ userId: req.user.sub, email: req.user.email });
});
// Role-restricted route
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
await deleteUser(req.params.id);
res.json({ deleted: true });
});
قائمة التحقق الأمنية الهامة
- لا تقم مطلقًا بتخزين الرموز المميزة في localStorage — عرضة لـ XSS. استخدم ملفات تعريف الارتباط httpOnly لرموز التحديث.
- استخدم أسرار منفصلة للوصول وتحديث الرموز. إذا تسرب سر الوصول، فستظل رموز التحديث آمنة.
- انتهاء صلاحية رمز الوصول القصير (15 دقيقة كحد أقصى). لا يمكن إبطال رموز الوصول طويلة الأمد بدون قائمة الرفض.
- تحديث تناوب الرمز المميز — يُصدر كل تحديث رمزًا مميزًا للتحديث جديدًا ويلغي الرمز القديم. يكتشف سرقة الرمز المميز.
- HTTPS فقط قيد الإنتاج – يجب تشفير JWTs أثناء النقل.
- التحقق من صحة مطالبات iss و aud – يمنع استخدام الرموز المميزة من إحدى الخدمات في خدمة أخرى.
الأسئلة المتداولة
س: الجلسات أم JWT – أيهما أفضل؟
ج: من الأسهل إبطال الجلسات (الحالة من جانب الخادم) على الفور. JWTs عديمة الحالة ويتم قياسها أفقيًا بدون مخزن جلسة مشترك. استخدم الجلسات لتطبيقات أبسط؛ JWT لواجهات برمجة التطبيقات التي يستهلكها عملاء الهاتف المحمول/SPA أو الخدمات الصغيرة.
س: كيف يمكنني إبطال JWT قبل انتهاء الصلاحية؟
ج: طريقتان: (1) الاحتفاظ بقائمة رفض Redis لرموز JTI الملغاة – يتم التحقق منها عند كل طلب. (2) تقصير مدة صلاحية رمز الوصول بحيث تكون نافذة التلف صغيرة. الخيار 1 أكثر أمانًا؛ الخيار 2 أبسط.
س: أين يجب تخزين رمز الوصول على العميل؟
ج: في الذاكرة (متغير JavaScript) – وليس التخزين المحلي أو تخزين الجلسة. تخزين الذاكرة يعني فقدانها عند تحديث الصفحة، مما يتطلب استدعاء رمز التحديث للحصول على رمز جديد – وهذا هو السلوك المقصود.
س: هل يجب علي استخدام RS256 أو HS256؟
ج: HS256 (السر المتماثل والمشترك) للخدمات التي تتحكم فيها بشكل كامل. RS256 (مفاتيح عامة/خاصة غير متماثلة) عندما تحتاج الجهات الخارجية إلى التحقق من الرموز المميزة دون معرفة سرك – مثل الخدمات الصغيرة أو الشركاء.
س: كيف يمكنني التعامل مع “الرمز المميز منتهي الصلاحية” على الواجهة الأمامية؟
ج: اعتراض الردود 401 بـcode: "TOKEN_EXPIRED" رأس، استدعاء/auth/refreshتلقائيًا ، واحصل على رمز وصول جديد، وأعد محاولة الطلب الأصلي. يدعم معظم عملاء HTTP (axios، ky) اعتراضات الطلب لهذا النمط.
الخلاصة
تتطلب مصادقة JWT للإنتاج أكثر من مجردjwt.sign() وjwt.verify(). يغطي التنفيذ الكامل: ملفات تعريف الارتباط httpOnly لرموز التحديث، وتدوير الرمز المميز عند كل تحديث، والإلغاء المدعوم من Redis لتسجيل الخروج، والأسرار المنفصلة لكل نوع رمز مميز، والبرامج الوسيطة للتفويض المستند إلى الدور. تتدرج هذه البنية أفقيًا (رموز الوصول عديمة الحالة)، وتتعامل مع سرقة الرمز المميز (يكشف التدوير عن إعادة الاستخدام)، وتدعم تسجيل الخروج المناسب (قائمة الإلغاء). ابدأ بهذا الأساس ولن تحتاج إلى إعادة هيكلة نظام المصادقة الخاص بك لاحقًا.
🔗 Share this article
✍️ Leave a Comment