路由

基础路由

Laravel 路由最基本的形式就是一个 URI 加一个闭包,简单直观,不需要复杂的路由配置文件:

use Illuminate\Support\Facades\Route;

Route::get('/greeting', function () {
    return 'Hello World';
});

默认路由文件

所有 Laravel 路由都定义在 routes 目录下的路由文件里。这些文件会被 Laravel 按照 bootstrap/app.php 里的配置自动加载。routes/web.php 定义的是 Web 界面的路由——这些路由会自动套上 web 中间件组,附带 session、CSRF 保护等功能。

大多数应用都会从 routes/web.php 开始写路由。这个文件里定义的路由,在浏览器输入对应的 URL 就能访问。比如下面这个路由,浏览器访问 http://example.com/user 就行了:

use App\Http\Controllers\UserController;

Route::get('/user', [UserController::class, 'index']);

API 路由

如果你的应用还要提供无状态的 API,用 install:api Artisan 命令就能开启 API 路由功能:

php artisan install:api

这个命令会安装 Laravel Sanctum,它提供了一套强大又简洁的 API token 认证守卫,适合第三方 API 消费者、SPA 或移动端应用。同时,install:api 会创建 routes/api.php 文件:

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

对需要公开访问的路由,去掉 auth:sanctum 中间件就行。

routes/api.php 里的路由是无状态的,属于 api 中间件组。另外,/api URI 前缀会自动加上,你不用给每个路由手写。想改前缀的话,在 bootstrap/app.php 里改:

->withRouting(
    api: __DIR__.'/../routes/api.php',
    apiPrefix: 'api/admin',
    // ...
)

可用的路由方法

路由支持注册响应任意 HTTP 动词的路由:

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

有时候你需要一个路由响应多个 HTTP 动词,可以用 match 方法。或者干脆用 any 方法响应全部动词:

Route::match(['get', 'post'], '/', function () {
    // ...
});

Route::any('/', function () {
    // ...
});

注意

定义多条共用同一个 URI 的路由时,get、post、put、patch、delete、options 这几个方法定义的路由要放在 any、match、redirect 方法定义的路由之前。这样才能保证请求匹配到正确的路由。

依赖注入

你可以在路由回调的参数里类型提示任何你需要的依赖。Laravel 服务容器 会自动解析并注入这些依赖。比如,类型提示 Illuminate\Http\Request 类,当前 HTTP 请求就会自动注入:

use Illuminate\Http\Request;

Route::get('/users', function (Request $request) {
    // ...
});

CSRF 保护

记住:在 web 路由文件里定义的任何指向 POST、PUT、PATCH、DELETE 路由的 HTML 表单,都要带一个 CSRF token 字段,否则请求会被拒绝。更多细节看 CSRF 文档:

<form method="POST" action="/profile">
    @csrf
    ...
</form>

重定向路由

如果你要定义一个路由只是做跳转,可以用 Route::redirect 方法。它是个快捷方式,不用专门定义完整路由或者控制器来做简单跳转:

Route::redirect('/here', '/there');

默认返回 302 状态码。第三个可选参数可以自定义状态码:

Route::redirect('/here', '/there', 301);

或者用 Route::permanentRedirect 直接返回 301:

Route::permanentRedirect('/here', '/there');

注意

在重定向路由中使用路由参数时,destination 和 status 是 Laravel 的保留参数,不能用。

视图路由

如果路由只需要返回一个视图,可以用 Route::view 方法。跟 redirect 方法一样,它也是个快捷方式。view 方法第一个参数是 URI,第二个参数是视图名称。第三个可选参数可以传一个数组给视图:

Route::view('/welcome', 'welcome');

Route::view('/welcome', 'welcome', ['name' => 'Taylor']);

注意

在视图路由中使用路由参数时,view、data、status、headers 是 Laravel 的保留参数,不能用。

列出路由

route:list Artisan 命令能让你快速总览应用里定义的所有路由:

php artisan route:list

默认不会显示每条路由的中间件。加 -v 参数就能看到路由中间件和中间件组名称:

php artisan route:list -v

# 展开中间件组...
php artisan route:list -vv

也可以让 Laravel 只显示某个 URI 开头的路由:

php artisan route:list --path=api

加 --except-vendor 参数可以隐藏第三方包定义的路由:

php artisan route:list --except-vendor

反过来,加 --only-vendor 只看第三方包的路由:

php artisan route:list --only-vendor

自定义路由加载

默认情况下,应用的路由由 bootstrap/app.php 配置和加载:

<?php

use Illuminate\Foundation\Application;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )->create();

有时候你可能想用一个全新的文件来放某类路由。给 withRouting 方法传一个 then 闭包就行,在里面注册你需要的额外路由:

use Illuminate\Support\Facades\Route;

->withRouting(
    web: __DIR__.'/../routes/web.php',
    commands: __DIR__.'/../routes/console.php',
    health: '/up',
    then: function () {
        Route::middleware('api')
            ->prefix('webhooks')
            ->name('webhooks.')
            ->group(base_path('routes/webhooks.php'));
    },
)

或者,你甚至可以完全接管路由注册——传一个 using 闭包给 withRouting。传了这个参数后,框架就不会自动注册任何 HTTP 路由了 ,你得自己手动注册所有路由:

use Illuminate\Support\Facades\Route;

->withRouting(
    commands: __DIR__.'/../routes/console.php',
    using: function () {
        Route::middleware('api')
            ->prefix('api')
            ->group(base_path('routes/api.php'));

        Route::middleware('web')
            ->group(base_path('routes/web.php'));
    },
)

then 还是 using?

then 是"在默认的基础上再加一份"——框架照常加载 web.php 和 api.php,你的闭包里额外注册一些路由。适合大多数场景:你只是多了一个路由文件要加载,不想改动默认行为。

using 是"我全权接管,你别管了"——一旦传了 using,withRouting 里写的 web:、api: 参数都会被忽略。你必须自己在闭包里把所有路由文件都 group / prefix / middleware 注册好,一条都不能漏。适合你想彻底重排路由结构的时候——比如把 web 和 api 路由合并到一个文件、用完全自定义的中间件组、或者按模块拆分路由且不用默认的 routes/ 目录结构。

简单说:只是想加个新路由文件 → then;想把框架默认的路由注册方式整个换掉 → using。

路由参数

必填参数

有时候你需要捕获 URL 中的部分内容。比如你想从 URL 里拿到用户的 ID,定义路由参数就行:

Route::get('/user/{id}', function (string $id) {
    return 'User '.$id;
});

定义多个路由参数也完全可以:

Route::get('/posts/{post}/comments/{comment}', function (string $postId, string $commentId) {
    // ...
});

路由参数必须用 {} 花括号包裹,由字母组成(下划线 _ 也可以)。路由参数按顺序注入路由回调或控制器——参数名本身不重要。

参数与依赖注入

如果路由既有依赖又有路由参数,把路由参数放在依赖之后:

use Illuminate\Http\Request;

Route::get('/user/{id}', function (Request $request, string $id) {
    return 'User '.$id;
});

可选参数

有时候需要一个参数可能不在 URL 中出现。在参数名后面加一个 ? 就行。别忘了给对应的变量一个默认值:

Route::get('/user/{name?}', function (?string $name = null) {
    return $name;
});

Route::get('/user/{name?}', function (?string $name = 'Yeetu') {
    return $name;
});

正则约束

你可以用路由实例的 where 方法限制参数的格式。where 方法接收参数名和一个正则表达式:

Route::get('/user/{name}', function (string $name) {
    // ...
})->where('name', '[A-Za-z]+');

Route::get('/user/{id}', function (string $id) {
    // ...
})->where('id', '[0-9]+');

Route::get('/user/{id}/{name}', function (string $id, string $name) {
    // ...
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

为了方便,一些常用的正则模式有对应的快捷方法:

Route::get('/user/{id}/{name}', function (string $id, string $name) {
    // ...
})->whereNumber('id')->whereAlpha('name');

Route::get('/user/{name}', function (string $name) {
    // ...
})->whereAlphaNumeric('name');

Route::get('/user/{id}', function (string $id) {
    // ...
})->whereUuid('id');

Route::get('/user/{id}', function (string $id) {
    // ...
})->whereUlid('id');

Route::get('/category/{category}', function (string $category) {
    // ...
})->whereIn('category', ['movie', 'song', 'painting']);

Route::get('/category/{category}', function (string $category) {
    // ...
})->whereIn('category', CategoryEnum::cases());

不满足约束的请求,Laravel 会自动返回 404。

全局约束

如果你想让某个路由参数永远被某个正则约束限制,可以用 pattern 方法。这些约束定义在 App\Providers\AppServiceProvider 的 boot 方法里:

use Illuminate\Support\Facades\Route;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Route::pattern('id', '[0-9]+');
}

定义之后,所有用这个参数名的路由都会自动应用这个约束:

Route::get('/user/{id}', function (string $id) {
    // 只有 {id} 是数字时才会执行...
});

编码正斜杠

Laravel 路由组件允许路由参数值中出现除了 / 之外的任意字符。如果你确实需要 / 出现在参数中,必须通过 where 正则显式放行:

Route::get('/search/{search}', function (string $search) {
    return $search;
})->where('search', '.*');

注意

编码正斜杠仅支持在最后一个路由段中使用。

什么场景需要参数里带 / ?

默认情况下,Laravel 用 / 来切分路由段,所以 /search/hello/world 里,{search} 只能抓到 hello,world 会当成下一段(通常就 404 了)。

加上 ->where('search', '.*') 后,{search} 会把后面的内容全部吞掉,包括斜杠。比如访问 /search/foo/bar/baz,$search 的值就是 foo/bar/baz。适合搜索功能、或者需要把一段完整路径传进路由的场景。

命名路由

命名路由让你方便地生成特定路由的 URL 或跳转。在路由定义后链式调用 name 方法就能起名:

Route::get('/user/profile', function () {
    // ...
})->name('profile');

控制器动作也能起名:

Route::get('/user/profile',[UserProfileController::class, 'show'])->name('profile');

注意

路由名称必须唯一。

生成命名路由的 URL

路由起了名之后,可以用 Laravel 的 route 和 redirect 辅助函数,通过路由名生成 URL 或跳转:

// 生成 URL...
$url = route('profile');

// 生成跳转...
return redirect()->route('profile');

return to_route('profile');

如果命名路由有参数,把参数作为 route 函数的第二个参数传进去。参数值会自动插入到生成 URL 的正确位置:

Route::get('/user/{id}/profile', function (string $id) {
    // ...
})->name('profile');

$url = route('profile', ['id' => 1]);

数组中多余的 key / value 对会自动加到生成 URL 的 query string 里:

Route::get('/user/{id}/profile', function (string $id) {
    // ...
})->name('profile');

$url = route('profile', ['id' => 1, 'photos' => 'yes']);

// http://example.com/user/1/profile?photos=yes

信息

有时候你可能想给 URL 参数设置请求级别的默认值,比如当前语言。可以用 URL::defaults 方法 来搞定。

检查当前路由

$request->route()->named('profile') 就是判断当前访问的 URL 对应的是不是名叫 profile 的那条路由。比如你有一条路由 Route::get('/user/{id}/profile', ...)->name('profile'),当用户访问 /user/1/profile 时,named('profile') 就返回 true。典型场景是在中间件里根据当前路由名做不同处理:

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Handle an incoming request.
 *
 * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
 */
public function handle(Request $request, Closure $next): Response
{
    if ($request->route()->named('profile')) {
        // ...
    }

    return $next($request);
}

路由组

路由组让你把中间件等路由属性共享给一大批路由,不用每条路由都写一遍。

嵌套的路由组会智能地"合并"属性和父组——中间件和 where 条件会合并,名称和前缀会追加。命名空间分隔符和 URI 前缀中的斜杠会在需要时自动添加。

中间件

给组内所有路由分配中间件,在定义组之前调用 middleware 方法就行。中间件按数组里列出的顺序执行:

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // 使用 first 和 second 中间件...
    });

    Route::get('/user/profile', function () {
        // 使用 first 和 second 中间件...
    });
});

控制器

如果一组路由都用同一个控制器,可以用 controller 方法指定公共控制器。然后定义路由时,你只需要提供控制器里要调用的方法名就行:

use App\Http\Controllers\OrderController;

Route::controller(OrderController::class)->group(function () {
    Route::get('/orders/{id}', 'show');
    Route::post('/orders', 'store');
});

子域名路由

路由组也可以用来处理子域名路由。子域名可以像路由 URI 一样带参数,这样你就能捕获子域名的一部分在路由或控制器里用。在定义组之前调用 domain 方法即可:

Route::domain('{account}.example.com')->group(function () {
    Route::get('/user/{id}', function (string $account, string $id) {
        // ...
    });
});

路由前缀

prefix 方法可以给组内每条路由的 URI 加一个前缀。比如你想给组内所有路由 URI 加上 admin:

Route::prefix('admin')->group(function () {
    Route::get('/users', function () {
        // 匹配 "/admin/users" URL
    });
});

路由名称前缀

name 方法可以给组内每条路由的名称加一个字符串前缀。比如你想给组内所有路由名加上 admin.,记得带上尾部 .:

Route::name('admin.')->group(function () {
    Route::get('/users', function () {
        // 路由被赋予名称 "admin.users"...
    })->name('users');
});

路由模型绑定

当你把模型 ID 注入路由或控制器时,通常还需要查数据库来找对应的模型。Laravel 路由模型绑定提供了一种便捷方式,直接把整个模型实例注入到路由里。比如,不注入用户的 ID,而是注入跟 ID 对应的完整 User 模型实例。

隐式绑定

Laravel 会自动解析路由或控制器动作里类型提示的 Eloquent 模型——只要变量名跟路由段名称一致。比如:

use App\Models\User;

Route::get('/users/{user}', function (User $user) {
    return $user->email;
});

因为 $user 变量类型提示了 App\Models\User Eloquent 模型,而且变量名跟 {user} URI 段匹配,Laravel 就会自动注入一个 ID 等于 URL 对应值的模型实例。如果数据库里找不到匹配的模型,会自动返回 404。

隐式绑定在控制器方法里也同样生效——注意 {user} URI 段跟控制器里类型提示了 App\Models\User 的 $user 变量匹配:

use App\Http\Controllers\UserController;
use App\Models\User;

// 路由定义...
Route::get('/users/{user}', [UserController::class, 'show']);

// 控制器方法定义...
public function show(User $user)
{
    return view('user.profile', ['user' => $user]);
}

软删除模型

默认情况下,隐式模型绑定不会检索已软删除的模型。如果你想让它也检索软删除的模型,在路由定义上链式调用 withTrashed 方法:

use App\Models\User;

Route::get('/users/{user}', function (User $user) {
    return $user->email;
})->withTrashed();

自定义主键

有时候你可能想用 id 以外的字段来解析 Eloquent 模型。在路由参数定义里指定字段就行:

use App\Models\Post;

Route::get('/posts/{post:slug}', function (Post $post) {
    return $post;
});

如果想让某个模型类的路由绑定始终使用某个字段而不是 id,在 Eloquent 模型上重写 getRouteKeyName 方法:

/**
 * Get the route key for the model.
 */
public function getRouteKeyName(): string
{
    return 'slug';
}

自定义主键与作用域

当你在一条路由定义里隐式绑定多个 Eloquent 模型时,你可能想限制第二个模型——让它必须是第一个模型的子模型。比如,下面这个路由定义会根据 slug 检索某个用户的博客文章:

use App\Models\Post;
use App\Models\User;

Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
    return $post;
});

当使用自定义主键的隐式绑定作为嵌套路由参数时,Laravel 会自动按约定猜出父模型上的关联名,从而限定查询范围。在上面这个例子里,Laravel 会假定 User 模型有一个名为 posts(路由参数名的复数形式)的关联关系,用它来检索 Post 模型。

如果你希望即使不提供自定义主键也让 Laravel 自动限定"子"绑定,可以在定义路由时调用 scopeBindings 方法:

use App\Models\Post;
use App\Models\User;

Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
    return $post;
})->scopeBindings();

或者给一整组路由都启用作用域绑定:

Route::scopeBindings()->group(function () {
    Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
        return $post;
    });
});

同样的,显式让 Laravel 不做作用域绑定,调用 withoutScopedBindings:

Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
    return $post;
})->withoutScopedBindings();

什么时候自动 scope,什么时候要手动加?

关键看路由参数里有没有 :。

  • {post:slug} 自动 scope:Laravel 解析 URI 时就把 post 的绑定字段记成了 slug(源码见 RouteUri::parse())。解析路由绑定时检测到 bindingFields 里有 post,自动走 scoped 路径:$user->posts()->where('slug', ...)->first()。不需要手动加 ->scopeBindings()。
  • {post} 不自动 scope:没带 : 就没有 bindingFields 记录,Laravel 不会自动限在父模型下面查。需要手动加 ->scopeBindings()。
  • 关联关系的名字怎么来的?源码 childRouteBindingRelationshipName() 会把参数名转成驼峰再变复数:'post' → 'posts'。所以你的 User 模型上必须有 posts() 这个关联方法,不然就报错。
  • ->withoutScopedBindings() 就是显式关掉 auto-scope——哪怕写了 {post:slug} 也不做父子限定,Post 全局查。

自定义模型未找到行为

默认情况下,隐式绑定的模型找不到时会返回 404。你可以通过 missing 方法自定义这个行为——传一个闭包,找不到绑定的模型时就会执行它:

use App\Http\Controllers\LocationsController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;

Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
    ->name('locations.view')
    ->missing(function (Request $request) {
        return Redirect::route('locations.index');
    });

隐式枚举绑定

PHP 8.1 引入了 枚举 支持。Laravel 也跟进了:你可以在路由定义里类型提示一个字符串型枚举,Laravel 只会在路由段匹配有效枚举值时才执行。否则自动返回 404。比如下面这个枚举:

<?php

namespace App\Enums;

enum Category: string
{
    case Fruits = 'fruits';
    case People = 'people';
}

你可以定义一个路由,只在 {category} 路由段为 fruits 或 people 时才执行。其他值自动返回 404:

use App\Enums\Category;
use Illuminate\Support\Facades\Route;

Route::get('/categories/{category}', function (Category $category) {
    return $category->value;
});

显式绑定

你不一定要用 Laravel 的隐式、约定式模型解析来做模型绑定。你也可以显式定义路由参数如何对应模型。注册显式绑定时,用路由的 model 方法为指定参数指定类。显式模型绑定应该定义在 AppServiceProvider 的 boot 方法开头:

use App\Models\User;
use Illuminate\Support\Facades\Route;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Route::model('user', User::class);
}

接下来,定义一条含有 {user} 参数的路由:

use App\Models\User;

Route::get('/users/{user}', function ($user) {
    // ...
});

因为我们把所有的 {user} 参数都绑定到了 App\Models\User 模型,所以这个类的实例会自动注入。比如请求 users/1 就会注入数据库中 ID 为 1 的 User 实例。

数据库里找不到匹配模型时,自动返回 404。

自定义解析逻辑

如果你想自定义模型绑定的解析逻辑,可以用 Route::bind 方法。传一个闭包进去,闭包会收到 URI 段的值,需要返回应该注入到路由里的类实例。同样,这个自定义应该放在 AppServiceProvider 的 boot 方法里:

use App\Models\User;
use Illuminate\Support\Facades\Route;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Route::bind('user', function (string $value) {
        return User::where('name', $value)->firstOrFail();
    });
}

或者,你也可以在 Eloquent 模型上重写 resolveRouteBinding 方法。这个方法会收到 URI 段的值,需要返回应该注入到路由里的类实例:

/**
 * Retrieve the model for a bound value.
 *
 * @param  mixed  $value
 * @param  string|null  $field
 * @return \Illuminate\Database\Eloquent\Model|null
 */
public function resolveRouteBinding($value, $field = null)
{
    return $this->where('name', $value)->firstOrFail();
}

如果路由用到了隐式绑定作用域,那么会用 resolveChildRouteBinding 方法来解析父模型的子绑定:

/**
 * Retrieve the child model for a bound value.
 *
 * @param  string  $childType
 * @param  mixed  $value
 * @param  string|null  $field
 * @return \Illuminate\Database\Eloquent\Model|null
 */
public function resolveChildRouteBinding($childType, $value, $field)
{
    return parent::resolveChildRouteBinding($childType, $value, $field);
}

回退路由

Route::fallback 方法可以定义一个路由——当所有路由都没有匹配到的时候执行它。通常情况下,未处理的请求会由应用的异常处理器自动渲染一个"404"页面。不过因为 fallback 路由一般定义在 routes/web.php 文件里,所以 web 中间件组的所有中间件也会对它生效。你也可以按需加额外的中间件:

Route::fallback(function () {
    // ...
});

速率限制

定义速率限制器

Laravel 内置了强大且可定制的速率限制服务,你可以用它来限制某个路由或路由组的访问频率。首先,在 App\Providers\AppServiceProvider 的 boot 方法里定义速率限制器的配置:

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

速率限制器通过 RateLimiter facade 的 for 方法定义。for 方法接收一个速率限制器的名称和一个闭包,闭包返回应该分配给该限制器的限制配置。限制配置是 Illuminate\Cache\RateLimiting\Limit 类的实例。这个类有一些方便的构建器方法,让你快速定义限制。名称可以是任何字符串:

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    RateLimiter::for('global', function (Request $request) {
        return Limit::perMinute(1000);
    });
}

如果请求超过了速率限制,Laravel 会自动返回 429 状态码。如果你想自定义返回的响应,用 response 方法:

RateLimiter::for('global', function (Request $request) {
    return Limit::perMinute(1000)->response(function (Request $request, array $headers) {
        return response('Custom response...', 429, $headers);
    });
});

因为速率限制器的回调会收到请求实例,你可以根据请求或认证用户动态构建合适的限制:

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()?->vipCustomer()
        ? Limit::none()
        : Limit::perHour(10);
});

细分速率限制

有时候你可能想按某个值来细分速率限制。比如你想让每个 IP 地址每分钟只能访问某个路由 100 次。构建限制时用 by 方法即可:

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->vipCustomer()
        ? Limit::none()
        : Limit::perMinute(100)->by($request->ip());
});

再举一个例子:已认证用户每分钟 100 次 / 按用户 ID 限制,访客每分钟 10 次 / 按 IP 限制:

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()
        ? Limit::perMinute(100)->by($request->user()->id)
        : Limit::perMinute(10)->by($request->ip());
});

多重速率限制

你可以在一个速率限制器里返回一个限制数组。每个限制会按照数组里的顺序被评估:

RateLimiter::for('login', function (Request $request) {
    return [
        Limit::perMinute(500),
        Limit::perMinute(3)->by($request->input('email')),
    ];
});

如果你分配了多个按相同 by 值细分的限制,确保每个 by 值唯一。最简单的方式是给 by 方法的值加个前缀:

RateLimiter::for('uploads', function (Request $request) {
    return [
        Limit::perMinute(10)->by('minute:'.$request->user()->id),
        Limit::perDay(1000)->by('day:'.$request->user()->id),
    ];
});

基于响应的速率限制

除了限制请求频率,Laravel 还支持基于响应来限制——用了 after 方法。当你只想把某些响应计入速率限制时(比如验证错误、404、或者其他特定 HTTP 状态码),这个很有用。

after 方法接收一个闭包,闭包收到响应后返回 true(计入速率限制)或 false(忽略)。这在防御枚举攻击时特别有用——比如限制连续 404 响应的频率;或者让验证失败的请求可以重试,不被计入限制:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;

RateLimiter::for('resource-not-found', function (Request $request) {
    return Limit::perMinute(10)
        ->by($request->user()?->id ?: $request->ip())
        ->after(function (Response $response) {
            // 仅把 404 响应计入限制,防止被遍历枚举...
            return $response->status() === 404;
        });
});

全局挂载 404 防遍历限流

上面这个限流器要全局生效,分两步:

1. 在 AppServiceProvider::boot() 里定义规则:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;

public function boot(): void
{
    RateLimiter::for('resource-not-found', function (Request $request) {
        return Limit::perHour(60)
            ->by($request->user()?->id ?: $request->ip())
            ->after(function (Response $response) {
                return $response->getStatusCode() === 404;
            });
    });
}

2. 在 bootstrap/app.php 的路由组上挂载中间件:

->withRouting(
    using: function () {
        Route::middleware('api')
            ->prefix('api')
            ->middleware('throttle:resource-not-found')
            ->group(base_path('routes/front/api.php'));

        Route::middleware('api')
            ->prefix('api/admin')
            ->middleware('throttle:resource-not-found')
            ->group(base_path('routes/admin/api.php'));
    },
)

行为说明:after 只控制哪些响应被计数(仅 404 消耗配额,正常请求不占坑),但计数器一满(每小时 60 次 404),所有请求都会直接返回 429——不管访问的是正常页面还是不存在页面。设计意图是:攻击者已被识别,整条线掐断。

将速率限制器挂载到路由上

速率限制器通过 throttle 中间件 挂载到路由或路由组上。throttle 中间件接收你要分配给路由的速率限制器名称:

Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/audio', function () {
        // ...
    });

    Route::post('/video', function () {
        // ...
    });
});

使用 Redis 限流

默认情况下,throttle 中间件映射到 Illuminate\Routing\Middleware\ThrottleRequests 类。但如果你用的是 Redis 作为缓存驱动,你可能想用 Redis 来管理速率限制。在 bootstrap/app.php 里调用 throttleWithRedis 方法就行——它会把 throttle 中间件映射到 Illuminate\Routing\Middleware\ThrottleRequestsWithRedis 中间件类:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->throttleWithRedis();
    // ...
})

表单方法伪造

HTML 表单不支持 PUT、PATCH、DELETE 动作。所以定义这些路由并通过 HTML 表单调用时,你得在表单里加一个隐藏的 _method 字段。_method 字段的值就是实际的 HTTP 请求方法:

<form action="/example" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>

更方便的做法是用 @method Blade 指令 生成 _method 输入字段:

<form action="/example" method="POST">
    @method('PUT')
    @csrf
</form>

获取当前路由

用 Route facade 的 current、currentRouteName、currentRouteAction 方法可以获取当前请求路由的信息:

use Illuminate\Support\Facades\Route;

$route = Route::current(); // Illuminate\Routing\Route
$name = Route::currentRouteName(); // string
$action = Route::currentRouteAction(); // string

完整的 API 方法列表请查阅 Route facade 底层类 和 Route 实例 的 API 文档。

跨域资源共享(CORS)

Laravel 可以自动用你配置的值响应 CORS OPTIONS HTTP 请求。OPTIONS 请求会由 HandleCors 中间件 自动处理,这个中间件已经包含在应用的全局中间件栈里了。

有时你可能需要自定义 CORS 配置。用 config:publish Artisan 命令发布 cors 配置文件:

php artisan config:publish cors

这个命令会在应用的 config 目录下创建一个 cors.php 配置文件。

信息

更多关于 CORS 和 CORS 头的信息,请参考 MDN 的 CORS 文档。

路由缓存

部署到生产环境时,你应该利用 Laravel 的路由缓存。路由缓存能大幅减少注册所有路由所需的时间。执行 route:cache Artisan 命令来生成路由缓存:

php artisan route:cache

执行后,每次请求都会加载缓存的路由文件。记住:添加了任何新路由后,都需要重新生成一次路由缓存。所以 route:cache 命令最好只在部署时跑。

要清除路由缓存,用 route:clear 命令:

php artisan route:clear