在构建 API 时,限流(Rate Limiting) 可以防止滥用和过度使用,保护后端服务;版本控制(Versioning) 则允许 API 在不破坏现有客户端的情况下平滑演进。Laravel 提供了内置的限流中间件和灵活的版本控制策略,让你轻松实现这些关键功能。
Laravel 的限流功能基于 Illuminate\Cache\RateLimiter,通过中间件 throttle 实现。你可以在 App\Http\Kernel 中定义全局限流中间件,或者在路由中单独指定。
在 app/Http/Kernel.php 的 $middlewareGroups 中的 api 组默认包含了 throttle:api 中间件:
protected $middlewareGroups = [
'api' => [
'throttle:api', // 默认限制 60 次/分钟
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
对应的限流配置定义在 App\Providers\RouteServiceProvider 的 configureRateLimiting 方法中:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
protected function configureRateLimiting()
{
// 默认 API 限流:每分钟 60 次
RateLimiter::for('api', function ($job) {
return Limit::perMinute(60)->by($job->user()?->id ?: $job->ip());
});
}
你可以为不同场景定义多个限流器,例如登录尝试、文件上传等。在 RouteServiceProvider 的 configureRateLimiting 中添加:
RateLimiter::for('login', function ($job) {
return Limit::perMinute(5)->by($job->ip());
});
RateLimiter::for('uploads', function ($job) {
return Limit::perMinute(10)->by($job->user()?->id ?: $job->ip());
});
然后在路由中使用这些限流器:
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:login');
Route::post('/upload', [FileController::class, 'upload'])->middleware('throttle:uploads');
你可以根据认证用户的不同等级设置不同的限制次数:
RateLimiter::for('api', function ($job) {
$user = $job->user();
$maxRequests = $user && $user->isPremium() ? 200 : 60;
return Limit::perMinute($maxRequests)->by($user?->id ?: $job->ip());
});
你可以直接在路由上使用 throttle 中间件并传递参数:throttle:次数,分钟。
// 每分钟最多 10 次
Route::get('/search', [SearchController::class, 'index'])->middleware('throttle:10,1');
也可以使用多个限流器组合:
// 先按 IP 限制 100 次/分钟,再按用户 ID 限制 10 次/分钟
Route::get('/api/data')->middleware('throttle:100,1', 'throttle:10,1:user_id');
当超过限制时,Laravel 默认返回 429 Too Many Requests 响应,并携带以下头信息:
X-RateLimit-Limit – 最大请求次数X-RateLimit-Remaining – 剩余次数Retry-After – 需要等待的秒数
你可以自定义限流响应的内容,通过抛出 ThrottleRequestsException 或自定义中间件实现。
CACHE_STORE=redis),因为限流器依赖缓存进行计数,Redis 的原子性操作更适合高频限流。
Laravel 本身没有内置强制的版本控制方式,但提供了多种灵活的实现模式。以下是三种常见策略:
将版本号直接放在 URL 中,例如 /api/v1/users、/api/v2/users。这种方式最直观,易于缓存和调试。
// routes/api.php
Route::prefix('v1')->group(function () {
Route::get('/users', [V1\UserController::class, 'index']);
});
Route::prefix('v2')->group(function () {
Route::get('/users', [V2\UserController::class, 'index']);
});
你可以将不同版本的控制器放在不同的命名空间下,例如 App\Http\Controllers\Api\V1 和 App\Http\Controllers\Api\V2。
客户端通过 Accept 头或自定义头(如 X-API-Version)指定版本,路由根据版本动态分发。
// 基于 Accept 头的示例: Accept: application/vnd.myapi.v1+json
Route::match(['get', 'post'], '/users', function () {
$version = request->header('Accept');
if (str_contains($version, 'v1')) {
return app()->call('App\Http\Controllers\Api\V1\UserController@index');
} elseif (str_contains($version, 'v2')) {
return app()->call('App\Http\Controllers\Api\V2\UserController@index');
}
abort(406, 'API version not supported');
});
更优雅的方式是使用中间件解析版本并动态调用控制器。
将版本放在子域名上,如 v1.api.example.com/users。需要在 config/routes.php 中配置子域名路由组。
Route::domain('v1.api.example.com')->group(function () {
Route::get('/users', [V1\UserController::class, 'index']);
});
当 API 返回的数据结构发生变化时,可以使用不同的 API 资源类来适配版本,而不必创建全新的控制器。
// 根据版本返回不同资源
if ($version === 'v1') {
return new V1\UserResource($user);
} else {
return new V2\UserResource($user);
}
你可以为不同版本的 API 设置不同的限流策略。例如,新版 API 可能更严格:
RateLimiter::for('api_v1', function ($job) {
return Limit::perMinute(100)->by($job->ip());
});
RateLimiter::for('api_v2', function ($job) {
return Limit::perMinute(50)->by($job->ip());
});
// 路由中分别应用
Route::prefix('v1')->middleware('throttle:api_v1')->group(...);
Route::prefix('v2')->middleware('throttle:api_v2')->group(...);
by($user->id ?: $request->ip()) 可以同时处理认证和未认证用户。Retry-After 和说明文档链接。
// app/Providers/RouteServiceProvider.php 中定义限流器
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
protected function configureRateLimiting()
{
RateLimiter::for('api_v1', function ($job) {
return Limit::perMinute(100)->by($job->user()?->id ?: $job->ip());
});
RateLimiter::for('api_v2', function ($job) {
return Limit::perMinute(80)->by($job->user()?->id ?: $job->ip());
});
}
// routes/api.php
Route::prefix('v1')->middleware('throttle:api_v1')->group(function () {
Route::apiResource('users', V1\UserController::class);
});
Route::prefix('v2')->middleware('throttle:api_v2')->group(function () {
Route::apiResource('users', V2\UserController::class);
});