تخطَّ إلى المحتوى

🔐 شرح أمن API وقواعد البيانات

مشروع ختامي: تأمين REST API

الدرس 23 من 25· ⏱ 8 دقائق قراءة

نظرة عامة

في هذا المشروع الختامي، ستطبق كل ما تعلمته عبر المسار لتأمين REST API. سنستخدم مشروع Express API (من مسار الباك إند) ونطبق عليه طبقات الأمان واحدة تلو الأخرى.

هيكل المشروع

secure-api/
├── src/
│   ├── middleware/
│   │   ├── auth.js          # JWT + RBAC
│   │   ├── rateLimiter.js   # Rate Limiting
│   │   ├── validator.js     # Input Validation
│   │   ├── audit.js         # Audit Logging
│   │   └── security.js      # Helmet + CORS + CSP
│   ├── routes/
│   │   ├── auth.routes.js
│   │   ├── users.routes.js
│   │   └── posts.routes.js
│   ├── models/
│   │   ├── user.model.js
│   │   └── post.model.js
│   ├── utils/
│   │   ├── jwt.js
│   │   └── encryption.js
│   └── app.js
├── tests/
│   ├── security.test.js
│   └── api.test.js
├── .env
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── .github/workflows/security.yml
└── package.json

الخطوة 1: JWT Hardening

// src/utils/jwt.js
const jwt = require('jsonwebtoken');

const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
const JWT_ALGORITHM = 'HS256';

function generateAccessToken(user) {
  return jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: ACCESS_TOKEN_EXPIRY, algorithm: JWT_ALGORITHM }
  );
}

function generateRefreshToken(user) {
  const token = crypto.randomBytes(64).toString('hex');
  // تخزين التوقيع في قاعدة البيانات (بدلاً من JWT نفسه)
  const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
  return { token, hashedToken };
}

function verifyToken(token, secret) {
  try {
    return jwt.verify(token, secret, {
      algorithms: [JWT_ALGORITHM],
      issuer: 'secure-api',
    });
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new AppError(401, 'انتهت صلاحية التوكن');
    }
    throw new AppError(401, 'توكن غير صالح');
  }
}
// src/middleware/auth.js
function authenticate(req, res, next) {
  const token = req.headers.authorization?.startsWith('Bearer ')
    ? req.headers.authorization.split(' ')[1]
    : req.cookies?.token;

  if (!token) {
    return res.status(401).json({ error: 'مطلوب توكن دخول' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
      algorithms: ['HS256'],
    });
    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ error: 'توكن غير صالح أو منتهي' });
  }
}

// RBAC Middleware
function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'مطلوب مصادقة' });
    }
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'ليس لديك صلاحية' });
    }
    next();
  };
}

الخطوة 2: Rate Limiting

// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');

// لكل IP عام
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 دقيقة
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'عدد كبير من الطلبات، حاول لاحقاً' },
});

// للمصادقة - صارم
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // ساعة
  max: 5, // 5 محاولات فقط
  skipSuccessfulRequests: true, // لا تحسب المحاولات الناجحة
  message: { error: 'محاولات كثيرة. انتظر ساعة.' },
  keyGenerator: (req) => req.ip,
});

// لكل مستخدم (مع Redis)
const userLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
    prefix: 'rate-limit:',
  }),
  windowMs: 60 * 1000, // دقيقة
  max: 30,
  keyGenerator: (req) => req.user?.userId || req.ip,
  handler: (req, res) => {
    res.status(429).json({
      error: 'تجاوزت حد الطلبات المسموح',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
    });
  },
});

الخطوة 3: إضافة Helmet و CSP

// src/middleware/security.js
const helmet = require('helmet');

const securityHeaders = helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", "data:"],
      connectSrc: ["'self'", "https://api.example.com"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      frameSrc: ["'none'"],
      formAction: ["'self'"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  noSniff: true,
  frameguard: { action: 'deny' },
  hidePoweredBy: true,
});

module.exports = { securityHeaders };
// تكوين CORS
const cors = require('cors');
const corsOptions = {
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400, // 24 ساعة
};

الخطوة 4: Input Validation

// src/middleware/validator.js
const { body, param, query, validationResult } = require('express-validator');

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'بيانات غير صالحة',
      details: errors.array().map(e => ({
        field: e.path,
        message: e.msg,
      })),
    });
  }
  next();
};

const userValidation = {
  create: [
    body('name').trim().isLength({ min: 2, max: 50 }).escape(),
    body('email').isEmail().normalizeEmail(),
    body('password')
      .isLength({ min: 8 })
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/),
    body('role').isIn(['user', 'admin']).optional(),
    validate,
  ],
  update: [
    param('id').isMongoId(),
    body('name').trim().isLength({ min: 2 }).optional(),
    body('email').isEmail().normalizeEmail().optional(),
    validate,
  ],
};

const postValidation = {
  create: [
    body('title').trim().isLength({ min: 3, max: 200 }).escape(),
    body('content').trim().isLength({ min: 10 }).escape(),
    validate,
  ],
};

الخطوة 5: Audit Logging

// src/middleware/audit.js
const auditLogger = (action) => {
  return async (req, res, next) => {
    const originalSend = res.send.bind(res);

    res.send = function (body) {
      const auditEntry = {
        timestamp: new Date().toISOString(),
        userId: req.user?.userId || 'anonymous',
        action,
        method: req.method,
        path: req.path,
        params: req.params,
        query: req.query,
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        statusCode: res.statusCode,
        responseSize: body?.length || 0,
      };

      // لا تسجل البيانات الحساسة
      delete auditEntry.params?.password;
      delete auditEntry.query?.token;

      // تخزين غير متزامن
      setImmediate(() => {
        db.auditLog.create({ data: auditEntry }).catch(console.error);
      });

      // تنبيه للسلوك المشبوه
      if (res.statusCode === 401 || res.statusCode === 403) {
        console.warn(`Suspicious: ${action} by ${auditEntry.userId}`, auditEntry);
      }

      return originalSend(body);
    };

    next();
  };
};

// استخدام
router.post('/login',
  authLimiter,
  auditLogger('LOGIN'),
  authController.login
);

router.delete('/users/:id',
  authenticate,
  authorize('admin'),
  auditLogger('DELETE_USER'),
  userController.delete
);

الخطوة 6: تطبيق كل شيء

// src/app.js - التجميع النهائي
const express = require('express');
const { securityHeaders } = require('./middleware/security');
const { globalLimiter } = require('./middleware/rateLimiter');
const { authenticate } = require('./middleware/auth');

const app = express();

// 1. رؤوس الأمان أولاً
app.use(securityHeaders);
app.use(cors(corsOptions));

// 2. Rate Limiting عام
app.use(globalLimiter);

// 3. Body parsing آمن
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));

// 4. NoSQL Injection protection
app.use((req, res, next) => {
  if (req.body && typeof req.body === 'object') {
    sanitizeMongoKeys(req.body);
  }
  next();
});

// 5. المسارات
app.use('/api/auth', authRoutes);
app.use('/api/users', authenticate, userRoutes);
app.use('/api/posts', authenticate, postRoutes);

// 6. 404
app.use((req, res) => {
  res.status(404).json({ error: 'المسار غير موجود' });
});

// 7. Error handler عالمي
app.use((err, req, res, next) => {
  console.error('Unhandled error:', err);
  res.status(err.status || 500).json({
    error: process.env.NODE_ENV === 'production'
      ? 'حدث خطأ داخلي'
      : err.message,
  });
});

الخطوة 7: اختبار الأمان

// tests/security.test.js
const request = require('supertest');
const app = require('../src/app');

describe('Security Tests', () => {
  describe('Headers', () => {
    test('يحتوي على Security Headers', async () => {
      const res = await request(app).get('/api/health');
      expect(res.headers['strict-transport-security']).toBeDefined();
      expect(res.headers['x-content-type-options']).toBe('nosniff');
      expect(res.headers['x-frame-options']).toBe('DENY');
      expect(res.headers['content-security-policy']).toBeDefined();
    });
  });

  describe('Rate Limiting', () => {
    test('يمنع الطلبات الكثيرة', async () => {
      const requests = Array(150).fill().map(() =>
        request(app).get('/api/health')
      );
      const results = await Promise.all(requests);
      const blocked = results.filter(r => r.status === 429);
      expect(blocked.length).toBeGreaterThan(0);
    });
  });

  describe('Input Validation', () => {
    test('يرفض SQL Injection', async () => {
      const res = await request(app)
        .post('/api/users')
        .send({ name: "1' OR '1'='1", email: "test@test.com" });
      expect(res.status).not.toBe(200);
    });

    test('يرفض XSS', async () => {
      const res = await request(app)
        .post('/api/posts')
        .send({ title: '<script>alert(1)</script>', content: 'test' });
      expect(res.body.title).not.toContain('<script>');
    });
  });

  describe('Authorization', () => {
    test('يمنع الوصول بدون توكن', async () => {
      const res = await request(app).get('/api/users');
      expect(res.status).toBe(401);
    });

    test('يمنع رفع الصلاحية', async () => {
      const userToken = await getTokenForRole('user');
      const res = await request(app)
        .delete('/api/users/123')
        .set('Authorization', `Bearer ${userToken}`);
      expect(res.status).toBe(403);
    });
  });
});

Dockerfile آمن

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs18:latest
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
USER nonroot
CMD ["src/app.js"]

CI/CD Security Pipeline

# .github/workflows/security.yml
name: Security Pipeline
on: [push, pull_request]
jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Dependency scan
        run: npm audit --audit-level=high

      - name: SAST scan
        uses: github/codeql-action/analyze@v2

      - name: Secret scan
        uses: trufflesecurity/trufflehog@v3

      - name: Container scan
        run: |
          docker build -t app:test .
          trivy image --severity HIGH,CRITICAL app:test

      - name: Generate SBOM
        run: |
          npm sbom --sbom-format=cyclonedx

وثيقة الأمان (Security Controls Document)

# Security Controls - Secure REST API

## المصادقة (Authentication)
- JWT access tokens (15min expiry)
- Refresh tokens مع rotation
- HttpOnly cookies للتخزين
- Rate limiting على /auth/login

## الصلاحيات (Authorization)
- RBAC مع أدوار user/admin
- BOLA protection على كل endpoint
- Middleware لكل route

## حماية البيانات (Data Protection)
- Input validation مع express-validator
- Output escaping
- NoSQL injection prevention
- تشفير PII في قاعدة البيانات

## الشبكة (Network)
- HTTPS إجباري
- HSTS preload
- CORS مقيد
- Helmet security headers
- CSP صارم

## التسجيل (Logging)
- Audit trail لكل عملية
- تسجيل المحاولات الفاشلة
- تنبيهات للسلوك المشبوه

## البنية التحتية (Infra)
- Docker مع non-root user
- Trivy scan في CI/CD
- Resource limits
- Read-only filesystem

⚠️ هذا المشروع يطبق معظم ما تعلمته. لا تتردد في الرجوع للدروس السابقة عند الحاجة. الأمان رحلة وليس وجهة.

🎯 التالي: قائمة تدقيق أمنية شاملة

شرح مشروع ختامي: تأمين REST API — أمن API وقواعد البيانات بالعربي
مشروع ختامي: تأمين REST APIأمن API وقواعد البيانات بالعربي · The Code Fix

هل كان هذا الدرس مفيدًا؟