آنچه در این مقاله میخوانید [پنهانسازی]
برای پیاده سازی Rate Limiting در Laravel روی API کافی است یک محدودکننده (Limiter) تعریف کنید و آن را با middleware به مسیرهای مورد نظر اعمال کنید. پیکربندی درست کلید شناسایی کاربر یا IP، انتخاب تعداد مجاز درخواست در یک بازه زمانی و سفارشی کردن پاسخ 429، سه تصمیم اصلی شما هستند.
Rate Limiting در Laravel دقیقا چیست و چه نیازی را برطرف میکند؟
Rate limiting یعنی محدود کردن تعداد درخواستهای مجاز هر کلید شناسایی (مانند IP یا شناسه کاربر) در یک بازه زمانی. با این کار از سوءاستفاده رباتها، حملات brute-force روی لاگین، فشار ناگهانی روی سرور و مصرف بیرویه منابع API جلوگیری میشود. در لاراول این قابلیت با RateLimiter و middleware داخلی throttle به سادگی در دسترس است.
پیش نیازهای کوتاه
- راه اندازی پایه یک پروژه Laravel و آشنایی مقدماتی با Route و Middleware.
- تنظیم درست cache driver. برای محیطهای چند سروری، Redis یا Memcached انتخاب بهتری از file/array هستند تا شمارندهها بین همه نودها مشترک باشند.
شروع سریع: فعال کردن محدودیت روی API
سادهترین راه این است که محدودکننده پیشفرض api را تعریف و روی مسیرهای API اعمال کنید. در بسیاری از اسکلتهای جدید لاراول یک limiter با نام api از پیش تعریف میشود. اگر ندارید یا میخواهید آن را شخصی سازی کنید، قطعه کد زیر را در RouteServiceProvider اضافه کنید.
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->configureRateLimiting();
// تعریف مسیرها...
}
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
// اگر کاربر لاگین است بر اساس user_id، در غیر این صورت بر اساس IP محدود کن
$key = $request->user()?->id
? 'user:'.$request->user()->id
: 'ip:'.$request->ip();
return [
Limit::perMinute(60)->by($key),
];
});
}
}
حالا همین limiter را روی مسیرهای api اعمال کنید:
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
Route::middleware('throttle:api')->group(function () {
Route::get('/ping', fn () => response()->json(['ok' => true]));
// سایر مسیرهای API...
});
نتیجه: هر کلید (user یا IP) حداکثر 60 درخواست در دقیقه روی این گروه میتواند ارسال کند. پس از عبور از سقف، پاسخ 429 Too Many Requests برگردانده میشود.
سفارشی سازی رفتار: پیام 429 و هدرهای مفید
Middleware داخلی throttle به طور خودکار پاسخ 429 را به همراه هدر Retry-After ارسال میکند. برای سفارشی کردن بدنه پاسخ 429 در تمام نقاط، میتوانید ThrottleRequestsException را هندل کنید:
<?php
// app/Exceptions/Handler.php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Throwable;
class Handler extends ExceptionHandler
{
public function register(): void
{
$this->renderable(function (ThrottleRequestsException $e, $request) {
$headers = method_exists($e, 'getHeaders') ? $e->getHeaders() : [];
return response()->json([
'message' => 'تعداد درخواستها از حد مجاز عبور کرده است.',
'retry_after' => $headers['Retry-After'] ?? null,
], 429, $headers);
});
}
}
نکته: روی پاسخهای موفق، معمولا هدرهای X-RateLimit-Limit و X-RateLimit-Remaining وجود دارند. با ابزارهایی مثل curl یا Postman آنها را بررسی کنید تا مطمئن شوید پیکربندی درست اعمال شده است.
سناریوهای عملی و الگوهای پیشنهادی
۱) محدودیت عمومی API
همان الگوی بالا (۶۰ در دقیقه) برای بیشتر APIهای عمومی مناسب است. اگر مسیرهایی بسیار ارزان (ارزان از نظر منابع) دارید، میتوانید سقف بالاتری بدهید.
۲) لاگین و احراز هویت
روی مسیرهای حساس مانند ورود (login) یا ارسال کد OTP تعداد کمی درخواست در دقیقه منطقی است. یک limiter اختصاصی تعریف و فقط روی همان مسیر اعمال کنید:
<?php
// RouteServiceProvider
RateLimiter::for('login', function (Request $request) {
$key = 'login:' . $request->ip();
return Limit::perMinute(5)->by($key);
});
// routes/api.php
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:login');
۳) آپلود فایل یا عملیات پرهزینه
برای عملیات سنگین بهتر است سختگیرانهتر باشید و حتی چند محدودیت همزمان تعریف کنید:
<?php
RateLimiter::for('uploads', function (Request $request) {
return [
// حداکثر 10 آپلود در دقیقه به ازای هر کاربر
Limit::perMinute(10)->by('user:'.$request->user()?->id ?? 'guest'),
// و حداکثر 30 به ازای هر IP
Limit::perMinute(30)->by('ip:'.$request->ip()),
];
});
// routes/api.php
Route::post('/files', [FileController::class, 'store'])
->middleware('throttle:uploads');
۴) محدودیت پویا بر اساس نقش کاربر
کاربران داخلی یا پلنهای پولی میتوانند سقفهای بیشتری داشته باشند:
<?php
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if ($user && $user->role === 'premium') {
return Limit::perMinute(300)->by('user:'.$user->id);
}
return Limit::perMinute(60)->by($user? 'user:'.$user->id : 'ip:'.$request->ip());
});
اعمال روی مسیرها: جدید و قدیمی
روش توصیهشده، استفاده از limiterهای نامگذاریشده است: throttle:api یا throttle:login و… . اگر با پروژههای قدیمیتر سروکار دارید، ممکن است الگوی پارامتری قدیمی را ببینید:
<?php
// قدیمیتر: 60 درخواست در 1 دقیقه
Route::middleware('throttle:60,1')->get('/ping', PingController::class);
برای انعطافپذیری بیشتر و نگهداری آسان، به روش نامگذاریشده مهاجرت کنید.
چگونه نتیجه را بررسی کنیم؟ (Dev و Production)
با curl
# چند بار تکرار تا پر شدن سهمیه
for i in {1..65}; do curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/api/ping; done
# بررسی هدرهای باقیمانده
curl -I https://api.example.com/api/ping | grep -i rate
با تست Feature در Laravel
<?php
// tests/Feature/ApiRateLimitTest.php
namespace Tests\Feature;
use Tests\TestCase;
class ApiRateLimitTest extends TestCase
{
public function test_api_rate_limit_triggers_after_limit(): void
{
// فرض: limiter 'api' برابر 60 در دقیقه است
for ($i = 0; $i < 60; $i++) {
$this->getJson('/api/ping')->assertOk();
}
$this->getJson('/api/ping')
->assertStatus(429)
->assertHeader('Retry-After');
}
}
اگر چند نود دارید، تست را طوری اجرا کنید که همه درخواستها از یک client واقعی بگذرد تا هماهنگی cache مشترک (مثل Redis) را بسنجید.
کلید شناسایی درست: IP یا کاربر؟
انتخاب کلید rate limit مستقیما روی تجربه کاربر اثر میگذارد:
- APIهای عمومی بدون احراز هویت: IP بهترین گزینه است.
- APIهای احراز هویت شده: شناسه کاربر (user id) انتخاب مناسبتری است تا کاربران پشت NAT به اشتباه محدود نشوند.
- ترکیبی: اگر برخی درخواستها همزمان guest و auth دارند، ابتدا user id و در غیر این صورت IP را استفاده کنید (الگوی نمونه در بالا).
خطاهای رایج و نحوه پیشگیری
- اعمال محدودیت بر اساس IP پشت پروکسی یا CDN: اگر پشت پروکسی هستید، IP واقعی کاربر را با پیکربندی TrustProxies استخراج کنید تا همه کاربران پشت یک پروکسی مشترک، به اشتباه یک کلید دیده نشوند.
- عدم اشتراک شمارنده در چند نود: در استقرار چندسروری، cache توزیعشده (Redis/Memcached) لازم است؛ وگرنه هر نود سهمیه جداگانهای خواهد داشت.
- یکسان گرفتن همه مسیرها: مسیرهای حساس مانند login یا عملیات پرهزینه باید limiter اختصاصی و سختگیرانهتری داشته باشند.
- فراموشی reset window: در تستهای محلی، اگر limit را پر کردید، تا پایان پنجره (مثلا یک دقیقه) یا با flush کردن cache منتظر بمانید.
- مبهم بودن پاسخ 429: پاسخ JSON یکنواخت و قابل پردازش برای کلاینتها تعریف کنید (نمونه Handler بالا).
نکات امنیتی و محدودیتها
- Rate limiting جایگزین احراز هویت، مجوزدهی و اعتبارسنجی ورودی نیست؛ یک لایه دفاعی تکمیلی است.
- پنجره زمانی ثابت ممکن است burstهای کوتاه را بهتر از sliding window مدیریت نکند. اگر به الگوهای پیچیدهتر نیاز دارید، راهکارهای جانبی مانند gateway یا reverse proxy با الگوریتمهای پیشرفتهتر را بررسی کنید.
- برای مسیرهای idempotent، در کنار rate limit از cache پاسخ یا queue نیز استفاده کنید تا فشار کاهش یابد.
چک لیست اجرایی سریع
- انتخاب cache driver مشترک (ترجیحا Redis در تولید).
- تعریف limiterهای نامگذاریشده برای الگوهای مختلف ترافیک (api، login، uploads).
- استفاده از کلید مناسب: user id اگر در دسترس است، در غیر این صورت IP.
- اعمال limiterها با middleware روی مسیرهای مرتبط.
- سفارشی کردن پاسخ 429 در Handler برای JSON یکنواخت.
- تست با curl و Feature Test و بررسی هدرهای rate limit.
- پیکربندی TrustProxies در صورت استفاده از CDN/Proxy.
گام بعدی چیست؟
از یک limiter عمومی شروع کنید، سپس برای مسیرهای حساس limiterهای اختصاصی بسازید و با مانیتور کردن هدرهای باقیمانده و لاگها، سقفها را بر اساس الگوی مصرف واقعی تنظیم کنید. اگر مقیاس بالاتری دارید، هماهنگی cache توزیعشده و سیاستهای متفاوت برای نقشهای کاربری را هم اضافه کنید.







