التعامل مع API في Laravel – الجزء الخامس: ما هو Rate Limiting وكيفية حماية REST API
عند بناء REST API لا يكفي أن تكون Endpoints صحيحة وتعمل بسرعة، بل يجب كذلك التفكير في الطريقة التي يتم بها استهلاك موارد الخادم.
ماذا يحدث مثلًا إذا قام مستخدم أو برنامج آلي بإرسال آلاف الطلبات إلى Endpoint معين خلال فترة زمنية قصيرة؟
قد يؤدي ذلك إلى استهلاك موارد الخادم وقاعدة البيانات، وزيادة التكلفة، والتأثير على سرعة النظام بالنسبة إلى المستخدمين الآخرين.
لهذا يوفر Laravel نظامًا متكاملًا يسمى:
Rate Limitingيسمح لنا بتحديد عدد الطلبات التي يستطيع Client تنفيذها خلال فترة زمنية معينة.
في هذا الجزء من سلسلة Laravel API سنتعرف على Rate Limiting، وكيفية استخدام Middleware باسم throttle، وإنشاء Named Rate Limiters مختلفة للمستخدمين والزوار وعمليات تسجيل الدخول ورفع الملفات، بالإضافة إلى إنشاء Response مخصص عند تجاوز الحد المسموح.
جدول المحتويات
- ما هو Rate Limiting؟
- لماذا نحتاج إلى Rate Limiting؟
- هل Rate Limiting يمنع هجمات DDoS؟
- كيف يعمل Rate Limiting؟
- ما هو 429 Too Many Requests؟
- Middleware باسم throttle
- تطبيق Rate Limit مباشرة على Route
- فهم throttle:60,1
- تطبيق Rate Limiting على مجموعة Routes
- إنشاء Named Rate Limiter
- تعريف RateLimiter في AppServiceProvider
- التحديد حسب المستخدم أو IP
- حد مختلف للمستخدم والزائر
- حماية Login من Brute Force
- استخدام Email وIP في Login Rate Limit
- إنشاء Rate Limiter لرفع الملفات
- تطبيق أكثر من Limit
- إنشاء رسالة 429 مخصصة
- Rate Limit Headers
- علاقة Rate Limiting بالـCache
- استخدام Redis
- Rate Limiting مع API Versioning
- أفضل الممارسات
- أخطاء شائعة
- مثال عملي متكامل
- ملخص الدرس
- الخلاصة
ما هو 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 RequestsRate 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 المسموح بها خلال فترة زمنية. |
| throttle | Middleware المسؤول عن تطبيق Rate Limiting على Routes. |
| 429 | HTTP 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.
awesome!