التعامل مع API في Laravel – الجزء الخامس: ما هو Rate Limiting وكيفية حماية REST API

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

ماذا يحدث مثلًا إذا قام مستخدم أو برنامج آلي بإرسال آلاف الطلبات إلى Endpoint معين خلال فترة زمنية قصيرة؟

قد يؤدي ذلك إلى استهلاك موارد الخادم وقاعدة البيانات، وزيادة التكلفة، والتأثير على سرعة النظام بالنسبة إلى المستخدمين الآخرين.

لهذا يوفر Laravel نظامًا متكاملًا يسمى:

Rate Limiting

يسمح لنا بتحديد عدد الطلبات التي يستطيع Client تنفيذها خلال فترة زمنية معينة.

في هذا الجزء من سلسلة Laravel API سنتعرف على Rate Limiting، وكيفية استخدام Middleware باسم throttle، وإنشاء Named Rate Limiters مختلفة للمستخدمين والزوار وعمليات تسجيل الدخول ورفع الملفات، بالإضافة إلى إنشاء Response مخصص عند تجاوز الحد المسموح.

جدول المحتويات

  1. ما هو Rate Limiting؟
  2. لماذا نحتاج إلى Rate Limiting؟
  3. هل Rate Limiting يمنع هجمات DDoS؟
  4. كيف يعمل Rate Limiting؟
  5. ما هو 429 Too Many Requests؟
  6. Middleware باسم throttle
  7. تطبيق Rate Limit مباشرة على Route
  8. فهم throttle:60,1
  9. تطبيق Rate Limiting على مجموعة Routes
  10. إنشاء Named Rate Limiter
  11. تعريف RateLimiter في AppServiceProvider
  12. التحديد حسب المستخدم أو IP
  13. حد مختلف للمستخدم والزائر
  14. حماية Login من Brute Force
  15. استخدام Email وIP في Login Rate Limit
  16. إنشاء Rate Limiter لرفع الملفات
  17. تطبيق أكثر من Limit
  18. إنشاء رسالة 429 مخصصة
  19. Rate Limit Headers
  20. علاقة Rate Limiting بالـCache
  21. استخدام Redis
  22. Rate Limiting مع API Versioning
  23. أفضل الممارسات
  24. أخطاء شائعة
  25. مثال عملي متكامل
  26. ملخص الدرس
  27. الخلاصة

ما هو Rate Limiting؟

Rate Limiting هي آلية للتحكم في عدد العمليات أو الطلبات التي يستطيع Client تنفيذها خلال فترة زمنية محددة.

مثلًا يمكن أن نحدد أن المستخدم يستطيع تنفيذ:

60 requests / minute

أي 60 Request كحد أقصى خلال دقيقة.

إذا تجاوز الحد المسموح، يقوم Laravel برفض الطلبات الإضافية إلى أن تصبح هناك مساحة جديدة ضمن Rate Limit.

يمكن تبسيط الفكرة بالشكل التالي:

Client
   |
   | Request
   v
Rate Limiter
   |
   +--- Within limit ----> Controller
   |
   +--- Limit exceeded --> 429 Too Many Requests

لماذا نحتاج إلى Rate Limiting؟

توجد أسباب عديدة لاستخدام Rate Limiting داخل REST API.

منع إساءة استخدام API

قد يقوم Script أو Bot بإرسال آلاف الطلبات دون وجود حاجة حقيقية لذلك.

تقليل محاولات Brute Force

يمكن وضع Limit منخفض على Endpoints مثل:

POST /api/login

وبذلك يصبح تنفيذ عدد كبير جدًا من محاولات تسجيل الدخول خلال فترة قصيرة أصعب.

حماية موارد الخادم

كل Request قد يحتاج إلى:

  • PHP Process.
  • Database Query.
  • Cache Access.
  • External API Request.
  • Disk I/O.
  • CPU.
  • Memory.

لذلك فإن عددًا ضخمًا من Requests قد يؤدي إلى استهلاك موارد التطبيق.

تقليل التكلفة

خصوصًا عند استخدام خدمات Cloud أو APIs خارجية يتم احتساب تكلفتها بناءً على عدد العمليات.

توزيع الموارد بصورة عادلة

بدون Rate Limiting قد يستهلك مستخدم واحد نسبة كبيرة من الموارد المتاحة، بينما Rate Limiting يسمح بتطبيق سياسة أكثر عدالة بين المستخدمين.

هل Rate Limiting يمنع هجمات DDoS؟

Rate Limiting مهم كطبقة حماية، لكنه لا ينبغي اعتباره نظام حماية كاملًا من هجمات DDoS.

Laravel يعمل داخل طبقة التطبيق، وهذا يعني أن Request قد يكون وصل أصلًا إلى البنية التحتية أو Web Server قبل أن يقوم Laravel برفضه.

لذلك في الأنظمة العامة يفضل دمج Application Rate Limiting مع طبقات أخرى مثل:

  • CDN.
  • WAF.
  • Reverse Proxy.
  • Load Balancer.
  • Network Rate Limiting.
  • DDoS Protection.

يمكن النظر إلى Laravel Rate Limiting باعتباره وسيلة لحماية منطق التطبيق وموارده من الاستهلاك المفرط، وليس بديلًا عن حماية الشبكة.

كيف يعمل Rate Limiting؟

Laravel يحتاج إلى معرفة عدد المحاولات التي قام بها Client خلال الفترة الزمنية المحددة.

لذلك تعتمد آلية Rate Limiting على Cache.

يمكن أن يكون المفتاح مثلًا مرتبطًا بـ:

User ID

أو:

IP Address

أو:

Email Address

على سبيل المثال:

User 15
   |
   | Request #1
   | Request #2
   | Request #3
   |
   v
Rate Limiter Counter

عند تجاوز العدد المسموح يتم رفض الطلب مؤقتًا.

ما هو 429 Too Many Requests؟

إذا تجاوز Client الحد المسموح به، يتم إرجاع HTTP Status Code:

429 Too Many Requests

وهو يعني أن Client قام بإرسال Requests أكثر من العدد الذي يسمح به النظام خلال الفترة الزمنية المحددة.

يجب أن يكون تطبيق الهاتف أو Frontend قادرًا على التعامل مع هذه الحالة وعدم الاستمرار في إرسال Requests بصورة متكررة.

Middleware باسم throttle

يوفر Laravel Middleware جاهزًا باسم:

throttle

يمكن تطبيقه على Route واحدة أو Route Group.

مثلًا:

Route::middleware('throttle:60,1')
    ->get('/brands', [
        BrandController::class,
        'index'
    ]);

تطبيق Rate Limit مباشرة على Route

لنفترض أننا نريد السماح بـ60 Request فقط خلال دقيقة:

Route::get('/brands', [
    BrandController::class,
    'index'
])->middleware('throttle:60,1');

هذا يعني أن Laravel سيقوم بتطبيق Rate Limiting على هذا Route.

ماذا تعني throttle:60,1؟

في الصيغة التقليدية:

throttle:60,1

تعني:

60 Requests
خلال
1 Minute

وليس 60 Request في الثانية.

وبالتالي:

throttle:10,1

تعني:

10 Requests خلال دقيقة.

و:

throttle:100,5

تعني:

100 Request خلال خمس دقائق.

لكن في التطبيقات الحديثة يفضل غالبًا استخدام Named Rate Limiters لأنها أوضح وأكثر مرونة من وضع الأرقام داخل Routes.

تطبيق Rate Limiting على مجموعة Routes

يمكن حماية مجموعة Routes كاملة:

Route::middleware('throttle:60,1')
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });

في هذه الحالة يتم تطبيق Rate Limit على جميع Routes الموجودة داخل المجموعة.

إنشاء Named Rate Limiter

بدل كتابة:

throttle:60,1

يمكن إنشاء Limiter باسم واضح:

api

ثم استخدامه:

throttle:api

هذه الطريقة أكثر مرونة لأنها تسمح بكتابة Rules أكثر تعقيدًا داخل مكان مركزي.

تعريف Rate Limiter في AppServiceProvider

في Laravel الحديث يمكن تعريف Named Rate Limiters داخل:

app/Providers/AppServiceProvider.php

داخل دالة:

boot()

مثال:

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }


    public function boot(): void
    {
        RateLimiter::for(
            'api',
            function (Request $request) {

                return Limit::perMinute(60);

            }
        );
    }
}

الآن لدينا Named Rate Limiter باسم:

api

يمكن تطبيقه على Route:

Route::middleware('throttle:api')
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });

تحديد Rate Limit حسب المستخدم أو IP

في أغلب التطبيقات لا نريد أن يشترك جميع المستخدمين في Counter واحد.

بل نريد أن يمتلك كل مستخدم Rate Limit مستقلًا.

يمكن استخدام:

->by(...)

مثلًا:

RateLimiter::for(
    'api',
    function (Request $request) {

        return Limit::perMinute(60)
            ->by(
                $request->user()?->id
                ?: $request->ip()
            );

    }
);

إذا كان المستخدم مسجل الدخول، يتم استخدام:

User ID

وإذا لم يكن مسجلًا يتم استخدام:

IP Address

وهكذا يحصل كل مستخدم أو عنوان IP على Counter مستقل.

وضع حد مختلف للمستخدم المسجل والزائر

قد نريد منح المستخدم المسجل عدد Requests أكبر من Guest.

مثلًا:

RateLimiter::for(
    'api',
    function (Request $request) {

        if ($request->user()) {
            return Limit::perMinute(120)
                ->by(
                    'user:'.$request->user()->id
                );
        }

        return Limit::perMinute(30)
            ->by(
                'ip:'.$request->ip()
            );
    }
);

بهذا نحصل على:

Authenticated User:
120 requests / minute

Guest:
30 requests / minute

حماية Login من Brute Force

واحدة من أهم الأماكن التي يجب تطبيق Rate Limiting عليها هي:

POST /api/login

بدل استخدام Limit عام يمكن إنشاء Limiter خاص باسم:

login

مثلًا:

RateLimiter::for(
    'login',
    function (Request $request) {

        return Limit::perMinute(5)
            ->by($request->ip());

    }
);

ثم:

Route::post(
    '/login',
    [AuthController::class, 'login']
)->middleware('throttle:login');

الآن كل IP يستطيع تنفيذ خمس محاولات خلال الدقيقة وفق هذا المثال.

لماذا IP وحده ليس مثاليًا لتسجيل الدخول؟

إذا اعتمدنا على IP فقط، فقد يكون عدد كبير من المستخدمين خلف عنوان IP واحد، كما يحدث أحيانًا في الشركات أو الجامعات أو شبكات الهواتف.

وإذا اعتمدنا على البريد الإلكتروني فقط، يستطيع المهاجم تغيير البريد في كل محاولة.

لذلك يمكن بناء مفتاح يجمع أكثر من معلومة.

مثلًا:

use Illuminate\Support\Str;

RateLimiter::for(
    'login',
    function (Request $request) {

        $email = Str::lower(
            (string) $request->input('email')
        );

        return Limit::perMinute(5)
            ->by(
                $email.'|'.$request->ip()
            );
    }
);

بهذا يصبح Limit مرتبطًا بمزيج من:

Email + IP Address

ويجب تحديد السياسة المناسبة حسب طبيعة التطبيق وحجم المستخدمين وطريقة تسجيل الدخول.

إنشاء Rate Limiter لرفع الملفات

رفع الملفات قد يستهلك موارد أكثر من Request عادي.

لذلك يمكن إنشاء Rate Limiter مستقل:

RateLimiter::for(
    'uploads',
    function (Request $request) {

        if ($request->user()) {
            return Limit::perMinute(20)
                ->by(
                    'user:'.$request->user()->id
                );
        }

        return Limit::perMinute(3)
            ->by(
                'ip:'.$request->ip()
            );
    }
);

ثم:

Route::post(
    '/uploads',
    [UploadController::class, 'store']
)->middleware('throttle:uploads');

بهذا نستطيع وضع سياسة مختلفة تمامًا لعملية Upload.

تطبيق أكثر من Rate Limit

Laravel يسمح بإرجاع أكثر من Limit داخل Named Rate Limiter.

هذه الإمكانية مفيدة عندما نريد الجمع بين حد عام وحد أكثر تحديدًا.

مثلًا لعملية Login:

RateLimiter::for(
    'login',
    function (Request $request) {

        return [
            Limit::perMinute(20)
                ->by(
                    'ip:'.$request->ip()
                ),

            Limit::perMinute(5)
                ->by(
                    'email:'.strtolower(
                        (string) $request->input('email')
                    )
                ),
        ];
    }
);

في هذا المثال لدينا Limit حسب IP وآخر حسب Email.

ويجب على Request اجتياز جميع Limits المطبقة.

إنشاء رسالة خطأ مخصصة

عند تجاوز Rate Limit يقوم Laravel بإرجاع:

429 Too Many Requests

لكن يمكن تخصيص Response الخاص بالـLimiter.

مثلًا:

RateLimiter::for(
    'uploads',
    function (Request $request) {

        return Limit::perMinute(10)
            ->by(
                $request->user()?->id
                ?: $request->ip()
            )
            ->response(function (
                Request $request,
                array $headers
            ) {

                return response()->json([
                    'message' => 'لقد تجاوزت عدد الطلبات المسموح به. حاول مرة أخرى لاحقًا.',
                ], 429, $headers);

            });
    }
);

الآن عند تجاوز العدد المحدد يحصل Client على JSON Response منظم بدل رسالة عامة.

مثلًا:

{
    "message": "لقد تجاوزت عدد الطلبات المسموح به. حاول مرة أخرى لاحقًا."
}

مع:

HTTP 429 Too Many Requests

Rate Limit Headers

عند تطبيق Rate Limiting يمكن أن تحتوي الاستجابة على Headers تساعد Client على معرفة حالة Rate Limit.

من المهم خصوصًا الانتباه إلى:

Retry-After

حيث يستطيع Client استخدامه لمعرفة المدة التي ينبغي انتظارها قبل إعادة المحاولة عند تجاوز Limit.

لذلك لا يُفضل أن يقوم Frontend عند الحصول على 429 بإعادة إرسال Request بشكل مستمر وفوري.

الأفضل تنفيذ استراتيجية انتظار مناسبة.

علاقة Rate Limiting بالـCache

Laravel Rate Limiter يعتمد على Cache لتخزين معلومات Attempts والمدة المتبقية للـLimit.

بشكل مبسط:

Request
   |
   v
RateLimiter
   |
   v
Cache
   |
   +--- attempts
   +--- expiration

لذلك يجب الانتباه إلى Cache Configuration عند تشغيل التطبيق في بيئة Production.

استخدام Redis مع Rate Limiting

في التطبيقات التي تستقبل عددًا كبيرًا من الطلبات، يعد Redis خيارًا شائعًا للـCache والعمليات المرتبطة بالـRate Limiting.

وتزداد أهمية استخدام Cache مركزي عندما يكون التطبيق يعمل على أكثر من Server.

مثلًا:

Load Balancer
      |
      +------ Server 1
      |
      +------ Server 2
      |
      +------ Server 3
               |
               v
             Redis

إذا كان كل Server يحتفظ بعداد Requests منفصل محليًا فقد تصبح Limits غير متناسقة.

أما باستخدام Cache مركزي مثل Redis فيمكن لمختلف Instances الاعتماد على نفس حالة Rate Limiting.

Rate Limiting مع API Versioning

في الدرس السابق تعرفنا على:

/api/v1
/api/v2

ويمكن تطبيق Rate Limiting على كل Version.

مثلًا:

Route::prefix('v1')
    ->middleware('throttle:api')
    ->group(function () {

        Route::apiResource(
            'brands',
            V1BrandController::class
        );

    });

ويمكن استخدام سياسة أخرى لـV2 عند الحاجة:

Route::prefix('v2')
    ->middleware('throttle:api-v2')
    ->group(function () {

        Route::apiResource(
            'brands',
            V2BrandController::class
        );

    });

لكن لا يجب إنشاء Rate Limiter مختلف لكل Version دون وجود سبب فعلي.

أفضل الممارسات عند استخدام Rate Limiting

ضع Limits مختلفة حسب نوع العملية

ليس منطقيًا أن تكون:

GET /products

بنفس حساسية:

POST /login

أو:

POST /password-reset

أو:

POST /upload

لذلك استخدم Named Limiters حسب وظيفة Endpoint.

لا تجعل Limits منخفضة جدًا

إذا كان تطبيق الهاتف يقوم بعدة Requests عند فتح الصفحة، فإن Limit منخفض جدًا قد يمنع المستخدم الحقيقي.

لا تجعلها مرتفعة لدرجة فقدان فائدتها

يجب تحديد القيم بناءً على الاستخدام الحقيقي للنظام.

اعتمد على User ID عندما يكون ذلك ممكنًا

للمستخدمين المسجلين غالبًا يكون User ID أكثر دقة من IP Address.

انتبه إلى المستخدمين خلف Shared IP

قد يكون مئات المستخدمين خلف عنوان IP عام واحد.

لذلك Rate Limiting حسب IP يحتاج إلى تصميم مدروس.

استخدم Rate Limiting على Endpoints الحساسة

خصوصًا:

  • Login.
  • Register.
  • Password Reset.
  • OTP Requests.
  • Email Verification.
  • File Uploads.
  • Search Endpoints المكلفة.
  • Endpoints التي تستدعي APIs مدفوعة.

راقب 429 Responses

إذا كان عدد كبير من المستخدمين الحقيقيين يحصلون على:

429

فربما تكون Limits غير مناسبة أو يوجد Client يقوم بإرسال Requests بصورة غير صحيحة.

أخطاء شائعة في Rate Limiting

الاعتقاد أن throttle:10,1 يعني 10 Requests في الثانية

في الصيغة التقليدية:

throttle:10,1

تعني 10 Requests خلال دقيقة واحدة.

اعتبار Rate Limiting بديلًا عن DDoS Protection

Rate Limiting داخل Laravel يعمل على مستوى Application ولا يغني عن الحماية على مستوى الشبكة أو Edge.

استخدام IP فقط في جميع الحالات

هذا قد يؤدي إلى مشاكل مع المستخدمين الموجودين خلف NAT أو Shared Networks.

استخدام Limit واحد لجميع Endpoints

لكل عملية تكلفة ومخاطر مختلفة، لذلك قد تحتاج إلى Policies مختلفة.

عدم التعامل مع 429 في Client

يجب أن يعرف Frontend أو تطبيق الهاتف كيف يتصرف عند:

429 Too Many Requests

ولا يقوم بإعادة إرسال الطلب فورًا بشكل متكرر.

تخزين العدادات محليًا في بيئة Multi-Server

في البنية الموزعة يجب استخدام Cache مشترك إذا أردنا Limits متناسقة بين Servers.

مثال عملي متكامل

لنفترض أن لدينا API يحتوي على:

  • Login.
  • Brands.
  • File Upload.

يمكن إعداد Rate Limiters داخل:

app/Providers/AppServiceProvider.php

بالشكل التالي:

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        RateLimiter::for(
            'api',
            function (Request $request) {

                return Limit::perMinute(60)
                    ->by(
                        $request->user()?->id
                        ?: $request->ip()
                    );
            }
        );


        RateLimiter::for(
            'login',
            function (Request $request) {

                $email = Str::lower(
                    (string) $request->input('email')
                );

                return Limit::perMinute(5)
                    ->by(
                        $email.'|'.$request->ip()
                    );
            }
        );


        RateLimiter::for(
            'uploads',
            function (Request $request) {

                return $request->user()
                    ? Limit::perMinute(20)
                        ->by(
                            'user:'.$request->user()->id
                        )
                    : Limit::perMinute(3)
                        ->by(
                            'ip:'.$request->ip()
                        );
            }
        );
    }
}

ثم داخل:

routes/api.php

نكتب:

<?php

use App\Http\Controllers\AuthController;
use App\Http\Controllers\BrandController;
use App\Http\Controllers\UploadController;
use Illuminate\Support\Facades\Route;


Route::post(
    '/login',
    [AuthController::class, 'login']
)->middleware('throttle:login');


Route::middleware('throttle:api')
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });


Route::post(
    '/uploads',
    [UploadController::class, 'store']
)->middleware([
    'auth:sanctum',
    'throttle:uploads',
]);

بهذه الطريقة لدينا ثلاث سياسات مستقلة:

Limiterالهدف
apiالطلبات العامة للـAPI
loginتقليل محاولات تسجيل الدخول المتكررة
uploadsالتحكم في عمليات رفع الملفات

ملخص الدرس

المفهومالوصف
Rate Limitingتحديد عدد Requests المسموح بها خلال فترة زمنية.
throttleMiddleware المسؤول عن تطبيق Rate Limiting على Routes.
429HTTP Status Code عند تجاوز الحد المسموح.
RateLimiter::for()إنشاء Named Rate Limiter.
Limit::perMinute()تحديد عدد Requests في الدقيقة.
by()تحديد المفتاح الذي يتم بناء Counter بناءً عليه.
User IDمفتاح مناسب غالبًا للمستخدم المسجل.
IP Addressيمكن استخدامه للزوار مع الانتباه إلى Shared IP.
response()تخصيص Response عند تجاوز Rate Limit.
Cacheيستخدمه Laravel لتتبع محاولات Rate Limiting.
Redisخيار مناسب للـCache خصوصًا في الأنظمة الموزعة.

الخلاصة

Rate Limiting من أهم الطبقات التي يجب التفكير فيها عند بناء REST API حقيقي باستخدام Laravel.

فبدل السماح لأي Client بإرسال عدد غير محدود من Requests، نستطيع تحديد سياسة واضحة مثل:

60 requests / minute

وعند تجاوزها يعيد Laravel:

429 Too Many Requests

تعرفنا على استخدام Middleware:

throttle

كما تعلمنا الطريقة الأكثر مرونة باستخدام:

RateLimiter::for()

والتي تسمح بإنشاء Named Rate Limiters مثل:

api
login
uploads

كما يمكن تحديد Limits اعتمادًا على:

  • User ID.
  • IP Address.
  • Email.
  • أو مزيج من عدة قيم.

وتعلمنا كذلك أن Rate Limiting مفيد في تقليل Brute Force وإساءة استخدام API وحماية موارد التطبيق، لكنه ليس بديلًا عن WAF أو DDoS Protection على مستوى البنية التحتية.

اختيار Rate Limits الصحيحة ليس مجرد قرار برمجي ثابت، بل يجب أن يعتمد على طبيعة Endpoint، وتكلفة العملية، وعدد المستخدمين، وسلوك التطبيقات التي تستخدم API.