七、使用中间件过滤请求
在本章中,将详细讨论中间件,并提供来自调节软件的示例。中间件是一种很好的机制,可以帮助将软件应用分成不同的层。为了说明这个原理,中间件围绕应用的最内部提供了几层保护,它可以被认为是内核。
在 Laravel 4 中,中间件被称为过滤器。这些过滤器被用在路由中,以执行在控制器之前的动作,如验证,其中用户将基于特定的标准被过滤。此外,过滤器可以在控制器之后。
在 Laravel 5 中,中间件的概念已经存在,但在 Laravel 4 中并不突出,现在它被带到了实际请求工作流的前台,并且可以以各种方式使用。把它想象成一个俄罗斯娃娃,每个娃娃代表应用中的一个层——拥有正确的凭证将允许我们更深入地进入应用。
HTTP 内核
位于app/Http/Kernel.php
的文件是管理程序内核配置的文件。基本结构如下:
<?php namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel {
/**
* The application's global HTTP middleware stack.
*
* @var array
*/
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken',
];
/**
* The application's route middleware.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => 'App\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'App\Http\Middleware\RedirectIfAuthenticated',
];
}
$middleware
数组是中间件类及其名称空间的列表,它在每个请求时执行。$routeMiddleware
数组是一个键和值数组,创建为别名列表,可以与路由一起使用来过滤请求。
基本中间件结构
路由中间件类实现了Middleware
接口:
<?php namespace Illuminate\Contracts\Routing;
use Closure;
interface Middleware {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next);
}
在任何实现这个基类的类中,必须有一个接受$request
的handle
方法以及一个Closure
。
中间件的基本结构如下:
<?php namespace Illuminate\Foundation\Http\Middleware;
use Closure;
use Illuminate\Contracts\Routing\Middleware;
use Illuminate\Contracts\Foundation\Application;
use Symfony\Component\HttpKernel\Exception\HttpException;
class CheckForMaintenanceMode implements Middleware {
/**
* The application implementation.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* Create a new filter instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->app->isDownForMaintenance())
{
throw new HttpException(503);
}
return $next($request);
}
}
在这里,CheckForMaintenanceMode
中间件顾名思义就是这样做的:handle
方法检查应用是否处于应用模式。应用的isDownForMaintenance
方法被调用,如果它返回true
,那么将返回一个 503 HTTP 异常,该方法的执行将停止。否则,带有$request
参数的$next
闭包返回到调用类。
类型
像CheckForMaintenanceMode
这样的中间件可以从$middleware
数组中移除,并移到$routeMiddleware
数组中,这样就不需要在每次请求时都执行它,而只需要从某个特定的路径执行。
路由中间件未展开
两个基于路由的中间件类出现在app/Http/Middleware/
的 Laravel 5 中。其中一个类叫做Authenticate
。它提供基本身份验证并使用合同。
关于路由,中间件位于路由和控制器之间:
默认中间件–身份验证类
一个名为Authenticate.php
的类有以下代码:
<?php namespace MyCompany\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate {
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest())
{
if ($request->ajax())
{
return response('Unauthorized.', 401);
}
else
{
return redirect()->guest('auth/login');
}
}
return $next($request);
}
}
首先要注意的是Illuminate\Contracts\Auth\Guard
,它处理检查用户是否登录的逻辑。它被注入到构造函数中。
合同
请注意契约的概念是一种新的方式,它使用接口来提供一个非约定类,从而将实际类与调用类分开。这提供了一个很好的分离层,并允许基础类在需要时很容易地切换出来,同时保持方法的参数和返回类型。
手柄
handle
班是真正工作完成的地方。$request
对象随$next
闭包一起传入。接下来会发生什么真的很简单,但很重要。该代码询问当前用户是否是来宾,即未经过身份验证或登录。如果用户没有登录,则该方法将不允许用户访问下一步。如果请求已经通过 Ajax 到达,那么 401 消息将返回给浏览器。
如果请求不是通过 Ajax 请求到达的,则代码假设请求是通过标准页面请求到达的,并且用户被定向到授权/登录页面,该页面允许用户登录到应用。否则,如果用户被认证(guest()
不等于true
,则$next
闭包以$request
对象为参数返回给软件应用。总而言之,只有在用户未通过身份验证的情况下,应用的执行才会停止;否则,继续执行。
需要记住的重要一点是,在这种情况下,$request
对象被返回给软件。
定制中间件–日志
使用 Artisan 创建定制中间件很简单。artisan
命令如下:
$ php artisan make:middleware LogMiddleware
我们的LogMiddleware
类需要添加到Http/Kernel.php
文件中的$middleware
数组中,如下所示:
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'MyCompany\Http\Middleware\LogMiddleware'
];
LogMiddleware
类是一个中间件类的名称,它将用于记录使用网站的用户。产生的类只有一个方法,即handle
。作为认证中间件,它接受$request
对象以及$next
闭包:
<?php namespace MyCompany\Http\Middleware;
use Closure;
class LogMiddleware {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}
在这种情况下,我们希望简单地记录用户标识和执行某个操作的日期和时间。将$request
对象分配给$response
对象,并返回$response
对象,而不是$next
。代码如下:
public function handle($request, Closure $next)
{
$response = $next($request);
Log::create(['user_id'=>\Auth::user()->id,'created_at'=>date("Y-
m-d H:i:s")]);
return $response;
}
对数模型
使用以下命令创建 Log
模型:
$php artisan make:model Log
通过使用受保护的$table
属性,将Log
模型设置为使用名为log
的表格,而不是logs
。接下来,通过将公共$timestamps
属性设置为false
,将模型设置为不使用时间戳。最后,通过将受保护的$fillable
属性设置为需要填充的字段数组,允许同时填充user_id
和created_at
字段,从而允许使用create
功能。经过上述修改后,该类将如下所示:
<?php namespace MyCompany;
use Illuminate\Database\Eloquent\Model;
class Log extends Model {
protected $table = 'log';
public $timestamps = false;
protected $fillable = ['user_id','created_at'];
}
我们也可以创建Log
模型作为多态模型,通过向Log
模型添加以下代码,允许它在多个上下文中使用:
public function loggable()
{
return $this->morphTo();
}
类型
有关这方面的更多信息,请参阅 Laravel 文档。
日志模型迁移
需要调整database/migrations/[date_time]_create_logs_table.php
迁移使用log
表而不是logs
。还需要创建两个字段:user_id
,一个无符号的小整数,created_at
,一个模仿 Laravel 时间戳格式的datetime
字段。代码如下:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateLogsTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('log', function(Blueprint $table)
{
$table->smallInteger('user_id')->unsigned();
$table->dateTime('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('log');
}
}
可终止中间件
在中,除了在请求到达之后或响应到达之后执行操作之外,甚至在响应被发送到浏览器之后也可以执行操作。该类添加了terminate
方法并实现了TerminableMiddleware
:
use Illuminate\Contracts\Routing\TerminableMiddleware;
class StartSession implements TerminableMiddleware {
public function handle($request, $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// Store the session data...
}
}
可终止的记录
我们可以在terminate
函数中轻松执行用户的日志记录,如下所示,因为日志记录可能是生命周期中发生的最后一个操作。代码如下:
<?php namespace MyCompany\Http\Middleware;
use Closure;
use Illuminate\Contracts\Routing\TerminableMiddleware;
use MyCompany\Log;
class LogMiddleware implements TerminableMiddleware {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
/**
* Terminate the request.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
*/
public function terminate($request, $response)
{
Log::create(['user_id'=>\Auth::user()- >id,'created_at'=>date("Y-m-d H:i:s")]);
}
}
代码已经被放入terminate
方法中,所以它在请求-响应路径之外,允许代码保持干净。
使用中间件
如果我们希望用户在能够执行某个操作之前必须经过身份验证,我们可以传递一个数组作为第二个参数,以middleware
作为密钥,强制路由调用AccommodationsController
的search
方法上的auth
中间件:
Route::get('search-accommodation',
['middleware' => 'auth','AccommodationsController@search']);
在这种情况下,如果未通过身份验证,用户将被重定向到登录页面。
路线组
路线可以组合在一起,共享同一个中间件。例如,如果我们想保护我们应用中的所有路由,我们可以创建一个路由组,只需传入键值对middleware
和auth
。代码如下:
Route::group(['middleware' => 'auth'], function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities', 'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::resource('rooms', 'RoomsController');
Route::resource('locations', 'LocationsController');
})
这保护了位于路由组内的每条路由的每种方法。
路由组中的多个中间件
如果需要对未经身份验证的用户提供更多保护,我们可以创建一个白名单,只允许特定 IP 地址范围内的用户访问应用。
以下命令将创建所需的中间件:
$ php artisan make:middleware WhitelistMiddleware
WhitelistMiddleware
类看起来是这样的:
<?php namespace MyCompany\Http\Middleware;
use Closure;
class WhitelistMiddleware {
private $whitelist = ['192.2.3.211'];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (in_array($request->getClientIp(),$this->whitelist)) {
return $next($request);
} else {
return response('Unauthorized.', 401);
}
}
}
这里,创建了一个“T2”私有“T0”数组,其中包含公司内部设置的 IP 地址列表。然后,将请求的远程端口与数组中的值进行比较,并允许通过返回$next
闭包来继续。否则,将返回未经授权的响应。
现在whitelist
中间件需要和auth
中间件结合。要在路由组中使用whitelist
中间件,需要创建中间件的别名,并将其插入到$routeMiddleware
数组中的app/Http/Kernel.php
文件中。代码如下:
protected $routeMiddleware = [
'auth' => 'MyCompany\Http\Middleware\Authenticate',
'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth',
'guest' => 'MyCompany\Http\Middleware\RedirectIfAuthenticated',
'log' => 'MyCompany\Http\Middleware\LogMiddleware',
'whitelist' => 'MyCompany\Http\Middleware\WhitelistMiddleware'
];
接下来,要将此添加到该路由组的中间件列表中,需要用一个数组替换字符串auth
,其内容为auth
和whitelist
。代码如下:
Route::group(['middleware' => ['auth','whitelist']], function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities',
'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::resource('rooms', 'RoomsController');
Route::resource('locations', 'LocationsController');
});
现在,即使用户登录,也不可能访问受保护的内容,除非该 IP 地址在白名单中。
此外,如果只希望将某些路由列入白名单,路由组可以嵌套如下:
Route::group(['middleware' => 'auth', function()
{
Route::resource('accommodations', 'AccommodationsController');
Route::resource('accommodations.amenities',
'AccommodationsAmenitiesController');
Route::resource('accommodations.rooms', 'AccommodationsRoomsController');
Route::resource('accommodations.locations', 'AccommodationsLocationsController');
Route::resource('amenities', 'AmenitiesController');
Route::group(['middleware' => 'whitelist'], function()
{
Route::resource('rooms', 'RoomsController');
});
Route::resource('locations', 'LocationsController');
});
这将需要对RoomsController
进行身份验证(auth
)和白名单,而路由组内的所有其他控制器将只需要身份验证。
中间件排除和包含
如果希望仅对某些路由执行身份验证或白名单,则应将构造器方法添加到控制器中,该类的middleware
方法可以如下使用:
<?php namespace MyCompany\Http\Controllers;
use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use Illuminate\Http\Request;
use MyCompany\Accommodation\Room;
class RoomsController extends Controller {
public function __construct()
{
$this->middleware('auth',['except' => ['index','show']);
}
第一个参数是Kernel.php
文件中$routeMiddleware
数组的键。第二个参数是键和值数组。选项有except
或only
。except
选项明显是排除,而only
选项是包含。在前面的例子中,auth
中间件将应用于除index
或show
方法之外的所有方法,这是两种读取方法(它们不修改数据)。相反,如果log
中间件应该应用在index
和show
上,那么将使用以下构造函数:
public function __construct()
{
$this->middleware('log',['only' => ['index','show']);
}
正如所料,这两种方法的应用如下,并且还添加了whitelist
中间件:
public function __construct()
{
$this->middleware('whitelist',['except' => ['index','show']);
$this->middleware('auth',['except' => ['index','show']);
$this->middleware('log',['only' => ['index','show']);
}
该代码将要求所有非读取操作的身份验证和白名单 IP 地址,同时将任何请求记录到index
和show
中。
结论
中间件可以巧妙地过滤请求,保护应用或 RESTful API 免受不需要的请求。它还可以执行日志记录,并重定向任何符合特定标准的请求。
中间件还可以为现有的应用提供额外的功能。例如,Laravel 提供EncryptCookies
和AddQueuedCookiesToResponse
中间件处理 cookies,而StartSession
和ShareErrorsFromSession
处理会话。
AddQueuedCookiesToResponse
中的代码不过滤请求,而是添加请求:
public function handle($request, Closure $next)
{
$response = $next($request);
foreach ($this->cookies->getQueuedCookies() as $cookie)
{
$response->headers->setCookie($cookie);
}
return $response;
}
总结
在这一章中,我们研究了中间件,它是一种对任何功能都有用的机制,这些功能要么应该为每个请求执行,要么应该附加到特定的路由上。这是一种灵活的机制,允许程序员将代码转换为接口,因为任何实现Middleware
接口的中间件类都必须包含handle
方法。遵循良好的开发原则不仅被鼓励,而且通过这种类型的结构被要求。
在下一章中,我们将讨论雄辩的 ORM。
版权属于:月萌API www.moonapi.com,转载请注明出处