路由
基础路由
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