تطوير واجهات REST API

تحديد معدل الطلبات والتخفيف في الـ API

20 دقيقة الدرس 11 من 35

مقدمة إلى تحديد معدل الطلبات في الـ API

يعد تحديد معدل الطلبات آلية أمان وأداء حاسمة تتحكم في عدد الطلبات التي يمكن للعميل إجراؤها إلى واجهة برمجة التطبيقات الخاصة بك خلال فترة زمنية محددة. إنه يحمي واجهة برمجة التطبيقات الخاصة بك من الإساءة، ويمنع استنفاد الموارد، ويضمن الاستخدام العادل عبر جميع العملاء. في هذا الدرس الشامل، سنستكشف ميزات تحديد المعدل المدمجة في Laravel ونتعلم كيفية تنفيذ استراتيجيات التخفيف المخصصة.

لماذا يهم تحديد معدل الطلبات

قبل الغوص في التنفيذ، دعونا نفهم لماذا يعد تحديد المعدل ضروريًا لأي واجهة برمجة تطبيقات إنتاجية:

فوائد تحديد معدل الطلبات:
  • الحماية من هجمات الحرمان من الخدمة: يمنع هجمات الحرمان من الخدمة عن طريق تحديد الطلبات المفرطة
  • إدارة الموارد: يحمي موارد الخادم (المعالج، الذاكرة، اتصالات قاعدة البيانات)
  • الاستخدام العادل: يضمن وصول متساوي للـ API لجميع المستخدمين
  • التحكم في التكاليف: يقلل من تكاليف البنية التحتية عن طريق منع الإساءة
  • جودة الخدمة: يحافظ على أداء ثابت للمستخدمين الشرعيين
  • تحقيق الدخل: يمكّن من التسعير المتدرج بناءً على حدود الاستخدام

الـ Throttle Middleware المدمج في Laravel

يوفر Laravel ميدل وير قوية للتخفيف من معدل الطلبات مباشرة. لنبدأ بالاستخدام الأساسي:

<?php // routes/api.php use Illuminate\Support\Facades\Route; // التخفيف الأساسي: 60 طلب في الدقيقة Route::middleware(['throttle:60,1'])->group(function () { Route::get('/users', [UserController::class, 'index']); Route::get('/posts', [PostController::class, 'index']); }); // معدل مختلف للمستخدمين المصادق عليهم Route::middleware(['auth:sanctum', 'throttle:100,1'])->group(function () { Route::post('/posts', [PostController::class, 'store']); Route::put('/posts/{id}', [PostController::class, 'update']); }); // تحديد معدل لكل مستخدم (حد المعدل لكل مستخدم مصادق عليه) Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () { Route::get('/profile', [ProfileController::class, 'show']); }); </pre>

فهم معلمات Throttle

تقبل الميدل وير throttle عدة أشكال من المعلمات:

// التنسيق: throttle:max_attempts,decay_minutes // 60 طلب في الدقيقة 'throttle:60,1' // 1000 طلب في الساعة (60 دقيقة) 'throttle:1000,60' // 10000 طلب في اليوم (1440 دقيقة) 'throttle:10000,1440' // استخدام محدد معدل مسمى (محدد في RouteServiceProvider) 'throttle:api' // معدل ديناميكي بناءً على خاصية المستخدم 'throttle:rate_limit,1'

محددات المعدل المخصصة

يسمح لك Laravel بتحديد محددات معدل مخصصة في App\Providers\RouteServiceProvider. هذا هو المكان الذي تكمن فيه القوة الحقيقية:

<?php // app/Providers/RouteServiceProvider.php namespace App\Providers; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; class RouteServiceProvider extends ServiceProvider { public function boot(): void { // محدد معدل API الافتراضي RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60) ->by($request->user()?->id ?: $request->ip()); }); // تحديد معدل متدرج بناءً على اشتراك المستخدم RateLimiter::for('tiered', function (Request $request) { $user = $request->user(); if (!$user) { // مستخدمون مجهولون: 10 طلبات في الدقيقة return Limit::perMinute(10)->by($request->ip()); } // حد المعدل بناءً على خطة المستخدم return match ($user->subscription_plan) { 'free' => Limit::perMinute(50)->by($user->id), 'pro' => Limit::perMinute(200)->by($user->id), 'enterprise' => Limit::perMinute(1000)->by($user->id), default => Limit::perMinute(30)->by($user->id), }; }); // حدود معدل متعددة (دمج في الدقيقة والساعة) RateLimiter::for('strict', function (Request $request) { return [ Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()), Limit::perHour(1000)->by($request->user()?->id ?: $request->ip()), ]; }); // تحديد معدل خاص بنقطة نهاية API RateLimiter::for('expensive', function (Request $request) { return Limit::perMinute(5) ->by($request->user()?->id ?: $request->ip()) ->response(function (Request $request, array $headers) { return response()->json([ 'error' => 'عمليات مكلفة كثيرة جدًا. يرجى المحاولة مرة أخرى لاحقًا.', 'retry_after' => $headers['Retry-After'] ?? 60, ], 429, $headers); }); }); } } </pre>
نصيحة احترافية: استخدم طريقة by() لتحديد كيفية تتبع حدود المعدل. تتضمن الاستراتيجيات الشائعة التتبع حسب معرف المستخدم أو عنوان IP أو مفتاح API أو مجموعة من السمات.

رؤوس استجابة حدود المعدل

يضيف Laravel تلقائيًا معلومات حد المعدل إلى رؤوس الاستجابة. يساعد فهم هذه الرؤوس العملاء على تنفيذ منطق إعادة المحاولة المناسب:

// رؤوس الاستجابة عند تنشيط تحديد المعدل: X-RateLimit-Limit: 60 // الحد الأقصى للمحاولات المسموح بها X-RateLimit-Remaining: 47 // المحاولات المتبقية في النافذة Retry-After: 45 // ثوان حتى إعادة تعيين حد المعدل X-RateLimit-Reset: 1708012800 // طابع زمني Unix عندما يتم إعادة تعيين الحد

إليك كيفية استهلاك هذه الرؤوس في عميل API الخاص بك:

<?php // مثال على عميل API يتعامل مع حدود المعدل class ApiClient { private int $remainingRequests = 0; private int $resetTimestamp = 0; public function makeRequest(string $url, array $data = []): array { // تحقق مما إذا كنا محدودين بالمعدل if ($this->remainingRequests === 0 && time() < $this->resetTimestamp) { $waitSeconds = $this->resetTimestamp - time(); throw new RateLimitException("محدود بالمعدل. أعد المحاولة خلال {$waitSeconds} ثانية."); } $response = Http::withToken($this->apiKey)->get($url, $data); // تحديث تتبع حد المعدل من الرؤوس $this->remainingRequests = (int) $response->header('X-RateLimit-Remaining'); $this->resetTimestamp = (int) $response->header('X-RateLimit-Reset'); if ($response->status() === 429) { $retryAfter = (int) $response->header('Retry-After', 60); throw new RateLimitException("تم تجاوز حد المعدل. أعد المحاولة بعد {$retryAfter} ثانية."); } return $response->json(); } } </pre>

أنماط تحديد المعدل المتقدمة

1. حدود معدل ديناميكية بناءً على تكلفة الطلب

بعض العمليات أكثر تكلفة من غيرها. قم بتنفيذ تحديد معدل قائم على التكلفة:

<?php // app/Providers/RouteServiceProvider.php RateLimiter::for('cost-based', function (Request $request) { $user = $request->user(); $cost = $request->route()?->getAction('cost') ?? 1; return Limit::perMinute(100) ->by($user?->id ?: $request->ip()) ->response(function () use ($cost) { return response()->json([ 'error' => 'تم تجاوز حد المعدل', 'cost' => $cost, ], 429); }); }); // routes/api.php Route::get('/search', [SearchController::class, 'search']) ->middleware('throttle:cost-based') ->defaults('cost', 5); // تكلفة نقطة النهاية هذه 5 نقاط Route::get('/users', [UserController::class, 'index']) ->middleware('throttle:cost-based') ->defaults('cost', 1); // تكلفة نقطة النهاية هذه نقطة واحدة </pre>

2. الحماية من الاندفاع بنوافذ منزلقة

منع هجمات الاندفاع مع السماح بالاستخدام العادي:

<?php RateLimiter::for('burst-protection', function (Request $request) { return [ // السماح بالاندفاعات: 10 طلبات في الثانية Limit::perSecond(10)->by($request->user()?->id ?: $request->ip()), // المعدل المستدام: 100 طلب في الدقيقة Limit::perMinute(100)->by($request->user()?->id ?: $request->ip()), // الحد اليومي: 10,000 طلب يوميًا Limit::perDay(10000)->by($request->user()?->id ?: $request->ip()), ]; }); </pre>

3. مجموعة قائمة على IP وقائمة على المستخدم

تتبع كل من IP والمستخدم للحماية الشاملة:

<?php RateLimiter::for('combined', function (Request $request) { $user = $request->user(); if ($user) { // المستخدمون المصادق عليهم: التتبع حسب معرف المستخدم و IP return [ Limit::perMinute(100)->by($user->id), Limit::perMinute(200)->by($request->ip()), // لكل IP (مستخدمون متعددون) ]; } // المستخدمون المجهولون: حدود أكثر صرامة على أساس IP return Limit::perMinute(20)->by($request->ip()); }); </pre>

ميدل وير Throttle مخصصة

للتحكم الكامل، قم بإنشاء ميدل وير تخفيف مخصصة:

<?php // app/Http/Middleware/ApiThrottle.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use Symfony\Component\HttpFoundation\Response; class ApiThrottle { public function handle(Request $request, Closure $next, int $maxAttempts = 60): Response { $key = $this->resolveRequestSignature($request); $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts); $decaySeconds = 60; // الحصول على عدد المحاولات الحالية $attempts = Cache::get($key, 0); if ($attempts >= $maxAttempts) { $retryAfter = $this->getRetryAfter($key, $decaySeconds); return response()->json([ 'error' => 'طلبات كثيرة جدًا', 'message' => "تم تجاوز حد المعدل البالغ {$maxAttempts} طلب في الدقيقة.", 'retry_after' => $retryAfter, ], 429)->withHeaders([ 'X-RateLimit-Limit' => $maxAttempts, 'X-RateLimit-Remaining' => 0, 'Retry-After' => $retryAfter, 'X-RateLimit-Reset' => time() + $retryAfter, ]); } // زيادة عدد المحاولات $this->incrementAttempts($key, $decaySeconds); $response = $next($request); // إضافة رؤوس حد المعدل return $this->addHeaders( $response, $maxAttempts, $maxAttempts - $attempts - 1, $decaySeconds ); } protected function resolveRequestSignature(Request $request): string { if ($user = $request->user()) { return 'throttle:' . $user->id; } return 'throttle:' . $request->ip(); } protected function resolveMaxAttempts(Request $request, int $default): int { // السماح للمستخدمين المميزين بحدود أعلى if ($user = $request->user()) { return $user->rate_limit ?? $default; } return $default; } protected function incrementAttempts(string $key, int $decaySeconds): void { if (Cache::has($key)) { Cache::increment($key); } else { Cache::put($key, 1, $decaySeconds); } } protected function getRetryAfter(string $key, int $decaySeconds): int { return Cache::get($key . ':timer', $decaySeconds); } protected function addHeaders(Response $response, int $limit, int $remaining, int $reset): Response { return $response->withHeaders([ 'X-RateLimit-Limit' => $limit, 'X-RateLimit-Remaining' => max(0, $remaining), 'X-RateLimit-Reset' => time() + $reset, ]); } } </pre>

تحديد المعدل القائم على قاعدة البيانات

لتطبيقات المؤسسات، قم بتخزين حدود المعدل في قاعدة البيانات:

<?php // Migration Schema::create('api_rate_limits', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('endpoint'); $table->integer('max_attempts')->default(60); $table->integer('decay_minutes')->default(1); $table->timestamps(); $table->unique(['user_id', 'endpoint']); }); // Model class ApiRateLimit extends Model { protected $fillable = ['user_id', 'endpoint', 'max_attempts', 'decay_minutes']; public function user() { return $this->belongsTo(User::class); } } // الاستخدام في RouteServiceProvider RateLimiter::for('database', function (Request $request) { $user = $request->user(); $endpoint = $request->route()?->getName(); if ($user && $endpoint) { $rateLimit = ApiRateLimit::where('user_id', $user->id) ->where('endpoint', $endpoint) ->first(); if ($rateLimit) { return Limit::perMinute($rateLimit->max_attempts) ->by($user->id); } } // الاحتياطي الافتراضي return Limit::perMinute(60)->by($request->ip()); }); </pre>
تحذير: يضيف تحديد المعدل القائم على قاعدة البيانات عبء استعلام على كل طلب. قم بتخزين تكوينات حد المعدل بشكل قوي لتقليل ضربات قاعدة البيانات.

المراقبة والتحليلات

تتبع انتهاكات حدود المعدل للأمان والتخطيط للسعة:

<?php // app/Listeners/LogRateLimitHit.php namespace App\Listeners; use Illuminate\Auth\Events\RateLimitHit; use Illuminate\Support\Facades\Log; class LogRateLimitHit { public function handle(RateLimitHit $event): void { Log::warning('تم تجاوز حد المعدل', [ 'ip' => request()->ip(), 'user_id' => auth()->id(), 'endpoint' => request()->path(), 'user_agent' => request()->userAgent(), ]); // اختياريًا تخزين في قاعدة البيانات للتحليلات RateLimitViolation::create([ 'user_id' => auth()->id(), 'ip_address' => request()->ip(), 'endpoint' => request()->path(), 'violated_at' => now(), ]); } } // التسجيل في EventServiceProvider protected $listen = [ \Illuminate\Auth\Events\RateLimitHit::class => [ LogRateLimitHit::class, ], ]; </pre>

اختبار محددات المعدل

اكتب اختبارات لضمان عمل تحديد المعدل بشكل صحيح:

<?php // tests/Feature/RateLimitingTest.php namespace Tests\Feature; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class RateLimitingTest extends TestCase { use RefreshDatabase; public function test_rate_limiting_works_for_guests() { // قم بإجراء 60 طلبًا (يجب أن ينجح) for ($i = 0; $i < 60; $i++) { $response = $this->getJson('/api/users'); $response->assertStatus(200); } // الطلب الـ 61 يجب أن يكون محدودًا بالمعدل $response = $this->getJson('/api/users'); $response->assertStatus(429); $response->assertHeader('Retry-After'); } public function test_authenticated_users_have_higher_limits() { $user = User::factory()->create(['subscription_plan' => 'pro']); // قم بإجراء 200 طلب (حد خطة pro) for ($i = 0; $i < 200; $i++) { $response = $this->actingAs($user)->getJson('/api/posts'); $response->assertStatus(200); } // الطلب الـ 201 يجب أن يكون محدودًا بالمعدل $response = $this->actingAs($user)->getJson('/api/posts'); $response->assertStatus(429); } public function test_rate_limit_headers_are_present() { $response = $this->getJson('/api/users'); $response->assertHeader('X-RateLimit-Limit'); $response->assertHeader('X-RateLimit-Remaining'); $response->assertHeader('X-RateLimit-Reset'); } } </pre>
تمرين عملي:
  1. أنشئ محدد معدل مخصص يسمح للمستخدمين المجانيين بـ 50 طلب/دقيقة، والمستخدمين المحترفين 200 طلب/دقيقة، ومستخدمي المؤسسات بوصول غير محدود
  2. نفذ نظام تحديد معدل "قائم على التكلفة" حيث تستهلك العمليات المكلفة (مثل عمليات البحث المعقدة) حصة أكبر من حد المعدل
  3. أضف تسجيل انتهاكات حد المعدل إلى واجهة برمجة التطبيقات الخاصة بك وأنشئ لوحة تحكم لتصور نقاط النهاية التي يتم تحديد معدلها بشكل متكرر
  4. اكتب ميدل وير ينفذ محدد معدل "نافذة منزلقة" بدلاً من نافذة Laravel الثابتة الافتراضية
  5. أنشئ فئة عميل API في JavaScript/TypeScript تحترم رؤوس حدود المعدل وتعيد المحاولة تلقائيًا مع التراجع الأسي

أفضل الممارسات

أفضل ممارسات تحديد المعدل:
  • اختر حدودًا مناسبة: وازن بين الأمان وسهولة الاستخدام بناءً على أنماط الاستخدام النموذجية لـ API الخاص بك
  • قم بتوصيل الحدود بوضوح: وثق حدود المعدل في وثائق API الخاصة بك
  • استخدم رسائل خطأ وصفية: ساعد العملاء على فهم متى يمكنهم إعادة المحاولة
  • نفذ حدودًا متدرجة: كافئ المستخدمين المصادق عليهم والخطط المدفوعة بحدود أعلى
  • راقب الانتهاكات: تتبع ضربات حد المعدل لتحديد الإساءة أو الحدود غير الكافية
  • ضع في اعتبارك بدلات الاندفاع: اسمح بانفجارات قصيرة مع فرض حدود معدل مستدامة
  • ضع تكوينات حد المعدل في ذاكرة التخزين المؤقت: تجنب استعلامات قاعدة البيانات على كل طلب
  • اختبر بدقة: تأكد من أن تحديد المعدل لا يحظر حركة المرور الشرعية

الملخص

في هذا الدرس، تعلمت كيفية تنفيذ تحديد معدل شامل لـ Laravel API الخاص بك. أنت الآن تفهم ميدل وير throttle المدمجة في Laravel، وكيفية إنشاء محددات معدل مخصصة، وتنفيذ حدود متدرجة بناءً على اشتراكات المستخدمين، ومعالجة رؤوس استجابة حد المعدل، ومراقبة الانتهاكات. يعد تحديد المعدل ضروريًا لبناء واجهات برمجة تطبيقات قوية وآمنة وقابلة للتطوير تحمي البنية التحتية الخاصة بك مع توفير وصول عادل لجميع المستخدمين.