Node.js و Express

التحقق من صحة الطلبات وتطهير البيانات

30 دقيقة الدرس 12 من 40

التحقق من صحة الطلبات وتطهير البيانات

التحقق من صحة مدخلات المستخدم وتطهيرها أمر بالغ الأهمية لبناء واجهات برمجية آمنة وموثوقة. يضمن التحقق أن البيانات تلبي المتطلبات المتوقعة، بينما ينظف التطهير البيانات لمنع الثغرات الأمنية مثل XSS وحقن SQL.

لماذا التحقق والتطهير؟

  • الأمان: منع هجمات الحقن (SQL، XSS، NoSQL)
  • سلامة البيانات: التأكد من أن البيانات تطابق التنسيق والنوع المتوقع
  • تجربة المستخدم: توفير رسائل خطأ واضحة
  • منطق الأعمال: فرض قواعد وقيود الأعمال
  • اتساق قاعدة البيانات: منع البيانات غير الصالحة من دخول قاعدة البيانات
تحذير: لا تثق أبدًا في مدخلات المستخدم. تحقق دائمًا من البيانات وطهرها على جانب الخادم، حتى لو كان لديك التحقق من جانب العميل.

Express-Validator

Express-validator هي مكتبة تحقق وتطهير شائعة مبنية على validator.js:

# تثبيت express-validator npm install express-validator

مثال التحقق الأساسي

لنتحقق من بيانات تسجيل المستخدم:

const express = require('express'); const { body, validationResult } = require('express-validator'); const app = express(); app.use(express.json()); // POST /api/users - إنشاء مستخدم مع التحقق app.post('/api/users', // قواعد التحقق [ body('username') .trim() .isLength({ min: 3, max: 20 }) .withMessage('اسم المستخدم يجب أن يكون 3-20 حرفًا') .isAlphanumeric() .withMessage('اسم المستخدم يجب أن يحتوي على أحرف وأرقام فقط'), body('email') .trim() .isEmail() .withMessage('عنوان البريد الإلكتروني غير صالح') .normalizeEmail(), body('password') .isLength({ min: 8 }) .withMessage('كلمة المرور يجب أن تكون 8 أحرف على الأقل') .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) .withMessage('كلمة المرور يجب أن تحتوي على حرف كبير وصغير ورقم ورمز خاص'), body('age') .optional() .isInt({ min: 13, max: 120 }) .withMessage('العمر يجب أن يكون بين 13 و 120'), body('website') .optional() .isURL() .withMessage('تنسيق URL غير صالح') ], // المعالج (req, res) => { // التحقق من نتائج التحقق const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, message: 'فشل التحقق', errors: errors.array() }); } // نجح التحقق، تابع إنشاء المستخدم const { username, email, password, age, website } = req.body; res.status(201).json({ success: true, message: 'تم إنشاء المستخدم بنجاح', data: { username, email } }); } ); app.listen(3000);

طرق التحقق الشائعة

يوفر Express-validator العديد من طرق التحقق:

// مدققات السلاسل النصية .isLength({ min: 5, max: 100 }) // فحص الطول .isEmpty() // فحص الفراغ .isAlpha() // أحرف فقط .isAlphanumeric() // أحرف وأرقام .isNumeric() // أرقام فقط .isEmail() // تنسيق البريد الإلكتروني .isURL() // تنسيق URL .isIP() // عنوان IP .isJSON() // JSON صالح .matches(/regex/) // تعبير نمطي مخصص // مدققات الأرقام .isInt({ min: 0, max: 100 }) // نطاق عدد صحيح .isFloat({ min: 0.0, max: 1.0 }) // نطاق عدد عشري .isDecimal() // رقم عشري // مدققات منطقية .isBoolean() // قيمة منطقية // مدققات التاريخ .isDate() // تاريخ صالح .isISO8601() // تنسيق تاريخ ISO .isBefore('2025-12-31') // تاريخ قبل .isAfter('2020-01-01') // تاريخ بعد // مدققات خاصة .isUUID() // تنسيق UUID .isJWT() // رمز JWT .isCreditCard() // رقم بطاقة ائتمان .isPostalCode('US') // الرمز البريدي .isMobilePhone('en-US') // رقم الهاتف

طرق التطهير

ينظف التطهير البيانات المدخلة ويحولها:

// تطهير السلاسل النصية .trim() // إزالة المسافات البيضاء .escape() // تشفير HTML (< > & ' " /) .unescape() // فك تشفير HTML .toLowerCase() // تحويل إلى أحرف صغيرة .toUpperCase() // تحويل إلى أحرف كبيرة .normalizeEmail() // تطبيع البريد الإلكتروني (أحرف صغيرة، إزالة النقاط من Gmail) .blacklist('chars') // إزالة الأحرف المحظورة .whitelist('chars') // الاحتفاظ فقط بالأحرف المسموحة // تطهير الأرقام .toInt() // تحويل إلى عدد صحيح .toFloat() // تحويل إلى عدد عشري .toBoolean() // تحويل إلى منطقي // تطهير التاريخ .toDate() // تحويل إلى كائن Date // تطهير مخصص .customSanitizer(value => { return value.replace(/[^a-zA-Z0-9]/g, ''); })
نصيحة: اربط التطهير قبل التحقق. على سبيل المثال، استخدم .trim() قبل .isLength() لتجنب حساب المسافات البيضاء.

مدققات مخصصة

أنشئ منطق التحقق المخصص للمتطلبات المحددة:

const { body, validationResult } = require('express-validator'); // مثال المدقق المخصص app.post('/api/users', [ body('username') .custom(async (value) => { // تحقق مما إذا كان اسم المستخدم موجودًا بالفعل في قاعدة البيانات const user = await User.findOne({ username: value }); if (user) { throw new Error('اسم المستخدم موجود بالفعل'); } return true; }), body('password') .custom((value, { req }) => { // الوصول إلى الحقول الأخرى باستخدام req if (value === req.body.username) { throw new Error('كلمة المرور لا يمكن أن تكون نفس اسم المستخدم'); } return true; }), body('passwordConfirm') .custom((value, { req }) => { if (value !== req.body.password) { throw new Error('كلمات المرور غير متطابقة'); } return true; }), body('birthDate') .custom((value) => { const age = calculateAge(value); if (age < 18) { throw new Error('يجب أن يكون عمرك 18 عامًا على الأقل'); } return true; }) ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } // تابع... } ); function calculateAge(birthDate) { const today = new Date(); const birth = new Date(birthDate); let age = today.getFullYear() - birth.getFullYear(); const monthDiff = today.getMonth() - birth.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { age--; } return age; }

التحقق من المصفوفات والكائنات المتداخلة

تحقق من هياكل البيانات المعقدة:

const { body, validationResult } = require('express-validator'); // التحقق من مصفوفة الكائنات app.post('/api/orders', [ // التحقق من المصفوفة body('items') .isArray({ min: 1 }) .withMessage('يجب أن يحتوي الطلب على عنصر واحد على الأقل'), // التحقق من كل عنصر في المصفوفة body('items.*.productId') .isInt() .withMessage('معرف المنتج غير صالح'), body('items.*.quantity') .isInt({ min: 1, max: 100 }) .withMessage('الكمية يجب أن تكون بين 1 و 100'), body('items.*.price') .isFloat({ min: 0.01 }) .withMessage('السعر يجب أن يكون موجبًا'), // التحقق من الكائن المتداخل body('shippingAddress.street') .trim() .notEmpty() .withMessage('الشارع مطلوب'), body('shippingAddress.city') .trim() .notEmpty() .withMessage('المدينة مطلوبة'), body('shippingAddress.zipCode') .isPostalCode('US') .withMessage('الرمز البريدي غير صالح') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } // معالجة الطلب... } ); // مثال على محتوى الطلب: // { // "items": [ // { "productId": 1, "quantity": 2, "price": 29.99 }, // { "productId": 5, "quantity": 1, "price": 49.99 } // ], // "shippingAddress": { // "street": "123 Main St", // "city": "New York", // "zipCode": "10001" // } // }

التحقق باستخدام Joi

Joi هي مكتبة تحقق شائعة أخرى بنهج مختلف:

# تثبيت Joi npm install joi
const Joi = require('joi'); // تحديد مخطط التحقق const userSchema = Joi.object({ username: Joi.string() .alphanum() .min(3) .max(20) .required(), email: Joi.string() .email() .required(), password: Joi.string() .min(8) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) .required() .messages({ 'string.pattern.base': 'كلمة المرور يجب أن تحتوي على حرف كبير وصغير ورقم ورمز خاص' }), age: Joi.number() .integer() .min(13) .max(120) .optional(), birthDate: Joi.date() .max('now') .required(), website: Joi.string() .uri() .optional(), role: Joi.string() .valid('user', 'admin', 'moderator') .default('user'), preferences: Joi.object({ newsletter: Joi.boolean().default(false), notifications: Joi.boolean().default(true) }).optional() }); // Middleware للتحقق باستخدام Joi const validateUser = (req, res, next) => { const { error, value } = userSchema.validate(req.body, { abortEarly: false, // إرجاع جميع الأخطاء، وليس فقط الأول stripUnknown: true // إزالة الحقول غير المعروفة }); if (error) { const errors = error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message })); return res.status(422).json({ success: false, message: 'فشل التحقق', errors }); } // استبدال req.body بالبيانات المتحقق منها والمطهرة req.body = value; next(); }; // استخدام middleware app.post('/api/users', validateUser, (req, res) => { // req.body الآن متحقق منه ومطهر res.status(201).json({ success: true, data: req.body }); });
ملاحظة: Joi أكثر تصريحية ويوفر إعادة استخدام المخطط، بينما يتكامل express-validator مباشرة مع مسارات Express. اختر بناءً على تفضيلك واحتياجات مشروعك.

إنشاء Middleware تحقق قابل لإعادة الاستخدام

نظم منطق التحقق في middleware قابل لإعادة الاستخدام:

// validators/userValidator.js const { body, param, validationResult } = require('express-validator'); // قواعد التحقق const userValidationRules = () => { return [ body('username') .trim() .isLength({ min: 3, max: 20 }) .withMessage('اسم المستخدم يجب أن يكون 3-20 حرفًا') .isAlphanumeric() .withMessage('اسم المستخدم يجب أن يكون أبجديًا رقميًا'), body('email') .trim() .isEmail() .withMessage('بريد إلكتروني غير صالح') .normalizeEmail(), body('password') .isLength({ min: 8 }) .withMessage('كلمة المرور يجب أن تكون 8 أحرف على الأقل') ]; }; const updateUserValidationRules = () => { return [ param('id') .isInt() .withMessage('معرف المستخدم غير صالح'), body('username') .optional() .trim() .isLength({ min: 3, max: 20 }) .isAlphanumeric(), body('email') .optional() .trim() .isEmail() .normalizeEmail() ]; }; // Middleware للتحقق من نتائج التحقق const validate = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, errors: errors.array() }); } next(); }; module.exports = { userValidationRules, updateUserValidationRules, validate }; // routes/users.js const express = require('express'); const router = express.Router(); const { userValidationRules, updateUserValidationRules, validate } = require('../validators/userValidator'); router.post('/', userValidationRules(), validate, createUser); router.put('/:id', updateUserValidationRules(), validate, updateUser); module.exports = router;

أفضل ممارسات التطهير

// طهر دائمًا مدخلات المستخدم app.post('/api/posts', [ body('title') .trim() // إزالة المسافات البيضاء .escape() // منع XSS .notEmpty() .withMessage('العنوان مطلوب'), body('content') .trim() .escape() // منع XSS .isLength({ min: 10 }) .withMessage('المحتوى يجب أن يكون 10 أحرف على الأقل'), body('tags') .optional() .customSanitizer(value => { // تنظيف وتطبيع مصفوفة العلامات if (Array.isArray(value)) { return value .map(tag => tag.trim().toLowerCase()) .filter(tag => tag.length > 0) .slice(0, 5); // تحديد إلى 5 علامات } return []; }) ], validate, createPost );
تحذير أمني: اهرب دائمًا من محتوى HTML ما لم تكن بحاجة صراحة للسماح بـ HTML وقد نفذت تطهير HTML مناسب باستخدام مكتبات مثل DOMPurify أو sanitize-html.

التحقق من تحميل الملفات

يتطلب التحقق من تحميل الملفات معالجة خاصة:

const multer = require('multer'); const path = require('path'); // تكوين multer const storage = multer.diskStorage({ destination: './uploads/', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }); const fileFilter = (req, file, cb) => { // قبول الصور فقط const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('نوع الملف غير صالح. مسموح فقط بـ JPEG و PNG و GIF'), false); } }; const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 // 5MB كحد أقصى } }); // نقطة نهاية التحميل مع التحقق app.post('/api/upload', upload.single('avatar'), [ body('description') .optional() .trim() .isLength({ max: 200 }) .withMessage('الوصف يجب أن يكون 200 حرف كحد أقصى') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } if (!req.file) { return res.status(400).json({ message: 'لم يتم تحميل ملف' }); } res.json({ success: true, file: req.file, description: req.body.description }); } );
تمرين: أنشئ واجهة برمجية للمنتجات مع التحقق الشامل:
  • POST /api/products - إنشاء منتج مع التحقق من: الاسم (3-100 حرف)، الوصف (10-500 حرف)، السعر (عدد عشري موجب)، الفئة (enum: Electronics, Clothing, Food, Books)، المخزون (عدد صحيح غير سالب)، الصور (مصفوفة URLs)، البيانات الوصفية (كائن متداخل)
  • نفذ مدقق مخصص للتحقق من وجود الفئة
  • طهر جميع مدخلات السلاسل النصية (trim, escape)
  • أنشئ middleware تحقق قابل لإعادة الاستخدام
  • أرجع رسائل خطأ سهلة الاستخدام
  • اختبر مع بيانات صالحة وغير صالحة

تنسيق أخطاء التحقق

قم بتنسيق أخطاء التحقق لتجربة مستخدم أفضل:

const formatValidationErrors = (errors) => { return errors.array().reduce((acc, error) => { // تجميع الأخطاء حسب الحقل if (!acc[error.path]) { acc[error.path] = []; } acc[error.path].push(error.msg); return acc; }, {}); }; app.post('/api/users', userValidationRules(), (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, message: 'فشل التحقق', errors: formatValidationErrors(errors) }); } // تابع... }); // تنسيق استجابة الخطأ: // { // "success": false, // "message": "فشل التحقق", // "errors": { // "username": ["اسم المستخدم يجب أن يكون 3-20 حرفًا", "اسم المستخدم يجب أن يكون أبجديًا رقميًا"], // "email": ["عنوان البريد الإلكتروني غير صالح"], // "password": ["كلمة المرور يجب أن تكون 8 أحرف على الأقل"] // } // }

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