Node.js و Express
المصادقة باستخدام JWT
المصادقة باستخدام JWT
توفر رموز الويب JSON (JWT) آلية مصادقة آمنة وعديمة الحالة لتطبيقات الويب الحديثة. في هذا الدرس، سنقوم بتنفيذ المصادقة المستندة إلى JWT في تطبيقات Express.js.
المصادقة مقابل التفويض
فهم الفرق بين هذين المفهومين أمر بالغ الأهمية:
- المصادقة: التحقق من هوية المستخدم (تسجيل الدخول بالبيانات)
- التفويض: تحديد ما يمكن للمستخدم الوصول إليه (الأذونات والأدوار)
تذكر: المصادقة تجيب على "من أنت؟" بينما التفويض يجيب على "ماذا يمكنك أن تفعل؟"
بنية JWT
يتكون JWT من ثلاثة أجزاء مفصولة بنقاط (.):
// تنسيق JWT: header.payload.signature
// مثال JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// 1. الرأس (الخوارزمية ونوع الرمز)
{
"alg": "HS256",
"typ": "JWT"
}
// 2. الحمولة (المطالبات/البيانات)
{
"userId": "1234567890",
"name": "John Doe",
"iat": 1516239022, // تاريخ الإصدار
"exp": 1516242622 // تاريخ انتهاء الصلاحية
}
// 3. التوقيع (التحقق)
// HMACSHA256(
// base64UrlEncode(header) + "." + base64UrlEncode(payload),
// secret
// )
ملاحظة أمنية: حمولات JWT مشفرة بـ Base64، وليست مشفرة. لا تخزن أبدًا معلومات حساسة مثل كلمات المرور في رموز JWT.
تثبيت مكتبة jsonwebtoken
تثبيت الحزمة المطلوبة:
npm install jsonwebtoken bcryptjs
إنشاء رموز JWT
إنشاء الرموز بعد تسجيل الدخول الناجح:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
// متغير بيئة للمفتاح السري
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d';
// تسجيل مستخدم جديد
exports.register = async (req, res) => {
try {
const { name, email, password } = req.body;
// التحقق من وجود المستخدم
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'المستخدم موجود بالفعل'
});
}
// تشفير كلمة المرور
const hashedPassword = await bcrypt.hash(password, 10);
// إنشاء المستخدم
const user = await User.create({
name,
email,
password: hashedPassword
});
// إنشاء رمز JWT
const token = jwt.sign(
{ userId: user._id, email: user.email },
JWT_SECRET,
{ expiresIn: JWT_EXPIRE }
);
res.status(201).json({
success: true,
token,
user: {
id: user._id,
name: user.name,
email: user.email
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// تسجيل دخول المستخدم
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// التحقق من المدخلات
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'يرجى توفير البريد الإلكتروني وكلمة المرور'
});
}
// البحث عن المستخدم (تضمين حقل كلمة المرور)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'بيانات الاعتماد غير صالحة'
});
}
// التحقق من كلمة المرور
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'بيانات الاعتماد غير صالحة'
});
}
// إنشاء الرمز
const token = jwt.sign(
{
userId: user._id,
email: user.email,
role: user.role
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRE }
);
res.json({
success: true,
token,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
التحقق من رموز JWT
إنشاء برمجية وسيطة للتحقق من الرموز على المسارات المحمية:
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// حماية المسارات - التحقق من JWT
exports.protect = async (req, res, next) => {
try {
let token;
// التحقق من وجود الرمز في الرؤوس
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
// استخراج الرمز من "Bearer TOKEN"
token = req.headers.authorization.split(' ')[1];
}
// التحقق من وجود الرمز
if (!token) {
return res.status(401).json({
success: false,
message: 'غير مصرح للوصول إلى هذا المسار'
});
}
try {
// التحقق من الرمز
const decoded = jwt.verify(token, JWT_SECRET);
// إضافة المستخدم إلى كائن الطلب
req.user = await User.findById(decoded.userId).select('-password');
if (!req.user) {
return res.status(401).json({
success: false,
message: 'المستخدم غير موجود'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'غير مصرح، فشل الرمز'
});
}
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// تفويض الأدوار
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: `دور المستخدم ${req.user.role} غير مصرح للوصول إلى هذا المسار`
});
}
next();
};
};
أفضل ممارسة: قم دائمًا بتخزين JWT_SECRET في متغيرات البيئة واستخدم سلسلة عشوائية قوية. لا تلتزم بالأسرار في التحكم في الإصدار.
حماية المسارات
تطبيق برمجية وسيطة للمصادقة لحماية المسارات:
// routes/users.js
const express = require('express');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
const userController = require('../controllers/userController');
// مسارات عامة
router.post('/register', userController.register);
router.post('/login', userController.login);
// مسارات محمية (تتطلب المصادقة)
router.get('/profile', protect, userController.getProfile);
router.put('/profile', protect, userController.updateProfile);
// مسارات المسؤول فقط (تتطلب المصادقة + دور المسؤول)
router.get('/admin/users',
protect,
authorize('admin'),
userController.getAllUsers
);
router.delete('/admin/users/:id',
protect,
authorize('admin'),
userController.deleteUser
);
// أدوار متعددة مسموح بها
router.get('/dashboard',
protect,
authorize('admin', 'moderator'),
userController.getDashboard
);
module.exports = router;
رموز التحديث
تنفيذ رموز التحديث لأمان أفضل:
// إنشاء رموز الوصول والتحديث
const generateTokens = (userId) => {
// رمز وصول قصير الأمد (15 دقيقة)
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// رمز تحديث طويل الأمد (7 أيام)
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
};
// تسجيل الدخول مع رمز التحديث
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({
success: false,
message: 'بيانات الاعتماد غير صالحة'
});
}
const { accessToken, refreshToken } = generateTokens(user._id);
// تخزين رمز التحديث في قاعدة البيانات
user.refreshToken = refreshToken;
await user.save();
// إرسال رمز التحديث ككوكي httpOnly
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 أيام
});
res.json({
success: true,
accessToken,
user: {
id: user._id,
name: user.name,
email: user.email
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// تحديث رمز الوصول
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.cookies;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: 'رمز التحديث غير موجود'
});
}
// التحقق من رمز التحديث
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// البحث عن المستخدم والتحقق من تطابق رمز التحديث
const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({
success: false,
message: 'رمز تحديث غير صالح'
});
}
// إنشاء رمز وصول جديد
const accessToken = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({
success: true,
accessToken
});
} catch (error) {
res.status(401).json({
success: false,
message: 'رمز تحديث غير صالح أو منتهي الصلاحية'
});
}
};
// تسجيل الخروج
exports.logout = async (req, res) => {
try {
// مسح رمز التحديث من قاعدة البيانات
await User.findByIdAndUpdate(req.user._id, {
refreshToken: null
});
// مسح الكوكي
res.clearCookie('refreshToken');
res.json({
success: true,
message: 'تم تسجيل الخروج بنجاح'
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
استراتيجية رمز التحديث:
- رمز الوصول: قصير الأمد (15 دقيقة)، يُرسل في رأس التفويض
- رمز التحديث: طويل الأمد (7 أيام)، يُخزن في كوكي httpOnly
- عندما ينتهي رمز الوصول، استخدم رمز التحديث للحصول على رمز وصول جديد
- تخزين رموز التحديث في قاعدة البيانات لتمكين الإلغاء
تدفق المصادقة الكامل
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'الاسم مطلوب']
},
email: {
type: String,
required: [true, 'البريد الإلكتروني مطلوب'],
unique: true,
lowercase: true
},
password: {
type: String,
required: [true, 'كلمة المرور مطلوبة'],
minlength: 8,
select: false // لا ترجع كلمة المرور افتراضيًا
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
refreshToken: {
type: String,
select: false
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
// إعداد app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const authRoutes = require('./routes/auth');
const app = express();
// البرمجيات الوسيطة
app.use(express.json());
app.use(cookieParser());
// المسارات
app.use('/api/auth', authRoutes);
// معالج الأخطاء
app.use(errorHandler);
module.exports = app;
اعتبارات أمنية:
- استخدم HTTPS في الإنتاج لمنع اعتراض الرموز
- قم بتعيين علامة httpOnly على الكوكيز لمنع هجمات XSS
- نفذ تحديد معدل على نقاط نهاية تسجيل الدخول
- استخدم أسرار عشوائية قوية لتوقيع الرموز
- تحقق من صحة جميع مدخلات المستخدم ونظفها
- نفذ القائمة السوداء للرموز لتسجيل الخروج
تمرين عملي
نفذ نظام مصادقة JWT كامل:
- أنشئ نقطة نهاية تسجيل المستخدم مع تشفير كلمة المرور
- نفذ نقطة نهاية تسجيل الدخول التي تُرجع رمز JWT
- أنشئ برمجية وسيطة للمصادقة لحماية المسارات
- أضف تفويضًا قائمًا على الأدوار (مستخدم، مسؤول)
- نفذ آلية رمز التحديث
- أنشئ نقطة نهاية تسجيل الخروج التي تمسح الرموز
- أضف وظيفة إعادة تعيين كلمة المرور مع JWT