七、使用中间件过滤请求

在本章中,将详细讨论中间件,并提供来自调节软件的示例。中间件是一种很好的机制,可以帮助将软件应用分成不同的层。为了说明这个原理,中间件围绕应用的最内部提供了几层保护,它可以被认为是内核。

在 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);

}

在任何实现这个基类的类中,必须有一个接受$requesthandle方法以及一个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。它提供基本身份验证并使用合同。

关于路由,中间件位于路由和控制器之间:

Route middleware unravelled

默认中间件–身份验证类

一个名为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_idcreated_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作为密钥,强制路由调用AccommodationsControllersearch方法上的auth中间件:

Route::get('search-accommodation',
  ['middleware' => 'auth','AccommodationsController@search']);

在这种情况下,如果未通过身份验证,用户将被重定向到登录页面。

路线组

路线可以组合在一起,共享同一个中间件。例如,如果我们想保护我们应用中的所有路由,我们可以创建一个路由组,只需传入键值对middlewareauth。代码如下:

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,其内容为authwhitelist。代码如下:

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数组的键。第二个参数是键和值数组。选项有exceptonlyexcept选项明显是排除,而only选项是包含。在前面的例子中,auth中间件将应用于除indexshow方法之外的所有方法,这是两种读取方法(它们不修改数据)。相反,如果log中间件应该应用在indexshow上,那么将使用以下构造函数:

  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 地址,同时将任何请求记录到indexshow中。

结论

中间件可以巧妙地过滤请求,保护应用或 RESTful API 免受不需要的请求。它还可以执行日志记录,并重定向任何符合特定标准的请求。

中间件还可以为现有的应用提供额外的功能。例如,Laravel 提供EncryptCookiesAddQueuedCookiesToResponse中间件处理 cookies,而StartSessionShareErrorsFromSession处理会话。

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。