二、基础组件 —— 中间件
简介
中间件为过滤进入应用的 HTTP 请求提供了一套便利的机制。例如,Laravel 内置了一个中间件来验证用户是否经过认证(如登录),如果用户没有经过认证,中间件会将用户重定向到登录页面,而如果用户已经经过认证,中间件就会允许请求继续往前进入下一步操作。
当然,除了认证之外,中间件还可以被用来处理很多其它任务。比如:CORS 中间件可以用于为离开站点的响应添加合适的头(跨域);日志中间件可以记录所有进入站点的请求,从而方便我们构建系统日志系统。
Laravel 框架自带了一些中间件,包括认证、CSRF 保护中间件等等。所有的中间件都位于 app/Http/Middleware
目录下。
定义中间件
要创建一个新的中间件,可以通过 Artisan 命令 make:middleware
php artisan make:middleware CheckToken
这个命令会在 app/Http/Middleware
目录下创建一个新的中间件类 CheckToken
,在这个中间件中,我们只允许提供的 token
等于指定值 tlar.com 的请求访问路由,否则,我们将跳转到百度:
如果 token != 'tlar.com'
,中间件会返回一个 HTTP 重定向到百度;否则,请求会被传递下去。将请求往下传递可以通过调用回调函数 $next
并传入当前 $request
。
注:此时只是定义好了中间件的逻辑,要让这个中间件生效,还要将其注册到指定路由中
理解中间件的最好方式就是将中间件看做 HTTP 请求到达目标动作之前必须经过的“层”,每一层都会检查请求并且可以完全拒绝它。
注:所有的中间都是在服务容器中解析,所以你可以在中间件的构造函数中类型提示任何依赖。
请求之前/之后的中间件
一个中间件是请求前还是请求后执行取决于中间件本身。
比如,以下中间件会在请求处理前执行一些任务:
<?php namespace App\Http\Middleware; use Closure; class BeforeMiddleware { public function handle($request, Closure $next) { // 执行动作 return $next($request); } }
下面这个中间件则会在请求处理后执行其任务:
<?php namespace App\Http\Middleware; use Closure; class AfterMiddleware { public function handle($request, Closure $next) { $response = $next($request); // 执行动作 return $response; } }
注册中间件
中间件分三类,分别是全局中间件、中间件组和指定路由中间件:
全局中间件
如果你想要定义的中间件在每一个 HTTP 请求时都被执行,只需要将相应的中间件类添加到 app/Http/Kernel.php
的数组属性 $middleware
中即可:
除非真的需要,否则一般不会把业务级别的中间件放到全局中间件中。
分配中间件到指定路由
如果你想要分配中间件到指定路由,首先应该在 app/Http/Kernel.php
文件中分配给该中间件一个 key
,默认情况下,该类的 $routeMiddleware
属性包含了 Laravel 自带的中间件,要添加你自己的中间件,只需要将其追加到后面并为其分配一个 key
,例如:
中间件在 HTTP Kernel 中被定义后,可以使用 middleware
方法将其分配到路由:
Route::get('/', function () { return 'token'; })->middleware('token');
中间件组
有时候你可能想要通过指定一个键名的方式将相关中间件分到同一个组里面,这样可以更方便地将其分配到路由中,这可以通过使用 HTTP Kernel 提供的 $middlewareGroups
属性实现。
Laravel 自带了开箱即用的 web
和 api
两个中间件组,分别包含可以应用到 Web 和 API 路由的通用中间件:
中间件组使用和分配单个中间件同样的语法被分配给路由和控制器动作。再次申明,中间件组的目的只是让一次分配给路由多个中间件的实现更加方便:
Route::get('/', function () { // })->middleware('web'); Route::group(['middleware' => ['web']], function () { // });
默认情况下, RouteServiceProvider
自动将中间件组 web
应用到 routes/web.php
文件,将中间件组 api
应用到 routes/api.php
:
当然可以自己设置自己的中间件组,以实现更灵活的中间件分配策略:
/** * 应用的中间件组. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ 'throttle:60,1', 'auth:api', ], #新增blog 'blog' => [ 'token', ] ];
修改 routes/web.php
下面的中间件分配方式:
Route::group(['middleware'=>['blog']],function(){ Route::get('/', function () { return view('welcome', ['name' => 'blog view']); }); Route::view('/view', 'welcome', ['website' => 'blog /view ']); });
这样访问 http://blog.tlar
和 http://blog.tlar/view
的时候都要带上 token=tlar.com
参数,否则就会跳转到百度。
中间件排序
在某些特殊场景下,你可能需要中间件按照特定顺序执行,但是一旦中间件已经分配到指定路由就没法控制它们的执行顺序了。
在这种情况下,你可以在 app/Http/Kernel.php
文件中通过 $middlewarePriority
属性来指定中间件的优先级:
/** * The priority-sorted list of middleware. * * This forces non-global middleware to always be in the given order. * * @var array */ protected $middlewarePriority = [ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\Authenticate::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Auth\Middleware\Authorize::class, ];
中间件参数
中间件还可以接收额外的自定义参数,例如,如果应用需要在执行给定动作之前验证认证用户是否拥有指定的角色,可以创建一个 CheckRole
来接收角色名作为额外参数。
额外的中间件参数会在 $next
参数之后传入中间件:
<?php namespace App\Http\Middleware; use Closure; class CheckRole { /** * 处理输入请求 * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string $role * @return mixed * translator http://laravelacademy.org */ public function handle($request, Closure $next, $role) { if (! $request->user()->hasRole($role)) { // Redirect... } return $next($request); } }
中间件参数可以在定义路由时通过 :
分隔中间件名和参数名来指定,多个中间件参数可以通过逗号分隔:
Route::put('post/{id}', function ($id) { // })->middleware('role:editor');
终端中间件
终端中间件,可以理解为一个善后的后台处理中间件。有时候中间件可能需要在 HTTP 响应发送到浏览器之后做一些工作,比如,Laravel 内置的 session
中间件会在响应发送到浏览器之后将 Session 数据写到存储器中,为了实现这个功能,需要定义一个终止中间件并添加 terminate
方法到这个中间件:
<?php namespace Illuminate\Session\Middleware; use Closure; class StartSession { public function handle($request, Closure $next) { return $next($request); } public function terminate($request, $response) { // 存储session数据... } }
terminate
方法将会接收请求和响应作为参数。定义了一个终端中间件之后,还需要将其加入到 app/Http/Kernel.php
文件的全局中间件列表中。
当调用中间件上的 terminate
方法时,Laravel 将会从服务容器中取出一个该中间件的新实例,如果你想要在调用 handle
和 terminate
方法时使用同一个中间件实例,则需要使用容器提供的 singleton
方法以单例的方式将该中间件注册到容器中。