Laravel5.1中间件概览

Published on 2017 - 04 - 02

Introducing Laravel’s Default Middleware

Open your project’s app/Http/Middleware directory and you’ll find three ready-made middleware solutions, including:

  • Authenticated.php: This middleware is used to confirm a user is signed into the application. If not, the user is redirected to the login page.
  • RedirectIfAuthenticated.php: This middleware is used to confirm a user is not signed into the application. If so, the user is redirected to the home page. See Chapter 7 for more information about this middleware.
  • VerifyCsrfToken.php: This middleware is used to manage CSRF protection. As of Laravel 5.1 you can disable CSRF protection on a per-URI basis by updating this middleware’s $except array, or by altogethre removing the middleware from the app/Http/Kernel.php’s $middleware array, and then selectively enabling it on a per-route basis.

The VerifyCsrfToken middleware is automatically enabled for every route, however the other two (Authenticated and RedirectIfAuthenticated) are intended to be selectively applied according to specific route requests. You’ll find the latter two route-level middlewares registered in app/Http/Kernel.php, in addition to a third middleware found under the Illuminate namespace:

protected $routeMiddleware = [
'auth' => \todoparrot\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \todoparrot\Http\Middleware\RedirectIfAuthenticated::class,
'iplogger' => \todoparrot\Http\Middleware\RequestLogger::class,
];

There are also several application-level middlewares, which you’ll find defined in app/Http/Kernel.php’s $middleware array:

protected $middleware = [
    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Cookie\Middleware\EncryptCookies::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \App\Http\Middleware\VerifyCsrfToken::class,
];

The EncryptCookies, AddQueuedCookiesToResponse, StartSession, and ShareErrorsFromSession middlewares are used by Laravel to manage various session-related features. The CheckForMaintenanceMode middleware is used to determine whether the site administrator has placed the application in maintenance mode. CheckForMaintenanceMode is identified as application-level middleware because you want all users to immediately be presented with the maintenance message should it be enabled, meaning this middleware must execute in conjunction with every request in order to respond accordingly. Finally, VerifyCsrfToken is defined as application-level middleware because anytime a CSRF token is submitted along with a form, the token must be verified, meaning the VerifyCsrfToken middleware must execute with every request to confirm whether one has been passed.

How Application-Level Middleware Works

Application-level middleware is intended to execute in conjunction with every request with the thinking that the event the middleware is intended to filter could occur anywhere within the application. In this section you’ll learn more about how application-level middleware works by examining the VerifyCsrfToken middleware internals. The VerifyCsrfToken.php file is located here:

vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php

If you open it you’ll find the following class:

 <?php 

 namespace Illuminate\Foundation\Http\Middleware;

 use Closure;
 use Illuminate\Support\Str;
 use Symfony\Component\HttpFoundation\Cookie;
 use Illuminate\Contracts\Encryption\Encrypter;
 use Illuminate\Session\TokenMismatchExcept”

 class VerifyCsrfToken {

   protected $encrypter;

   protected $except;

   public function __construct(Encrypter $encrypter)
   {
     $this->encrypter = $encrypter;
  }

   public function handle($request, Closure $next)
   {
       if ($this->isReading($request) || $this->shouldPassThrough($request) 
         || $this->tokensMatch($request)) {
           return $this->addCookieToResponse($request, $next($request));
       }

       throw new TokenMismatchException;
   }
   protected function shouldPassThrough($request)
   {
       foreach ($this->except as $except) {
           if ($request->is($except)) {
               return true;
           }
       }

       return false;
   }

   protected function tokensMatch($request)
   {
       $token = $request->input('_token') ?: 
         $request->header('X-CSRF-TOKEN');

       if (!$token && $header = $request->header('X-XSRF-TOKEN')) {
           $token = $this->encrypter->decrypt($header);
       }

       return Str::equals($request->session()->token(), $token);
   }

   protected function addCookieToResponse($request, $response)
   {
       $response->headers->setCookie(
           new Cookie('XSRF-TOKEN', $request->session()->token(), 
           time() + 60 * 120, '/', null, false, false)
       );

       return $response;
   }

   protected function isReading($request)
   {
       return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
   }
 }

All middleware implements a method named handle, which is responsible for processing the incoming request if the middleware implementation’s parameters for doing so are met. In the case of VerifyCsrfToken, the HTTP method used for the request must include HEAD, GET, or OPTIONS (handled by the isReading method) and the token passed in via the $request object’s input method must match that which was saved to the session when the form was originally generated. If they do match, a new cookie named XSRF-TOKEN is set which tells Laravel the tokens do indeed match, and the response is returned; if they don’t match an exception of type TokenMismatchException is thrown.

So the bottom line is that when implementing a middleware you’ll need to implement the Middleware contract, which at present just contains the single handle method. Provided you do that, and return the response as demonstrated in the above code, you’re free to modify or respond to the request in any way you please. I’ll show you a concrete example of examining and responding to a particular request in the later section, “Creating Your Own Middleware Solution”.

How Route-Level Middleware Works

Route-level middleware works identically to application-level middleware, except that you can configure it to selectively execute in conjunction with a specific route or set of routes. For example if you look at the default Auth controller (app/Http/Controllers/Auth/AuthController.php) you’ll see that the guest middleware (guest is defined as the alias for the RedirectIfAuthenticated middleware in app/Http/Kernel.php) is enabled in the class constructor:

public function __construct()
{
    $this->middleware('guest', ['except' => 'getLogout']);
}

This means that the RedirectIfAuthenticated middleware will intercept every request made to an endpoint associated with the Auth controller except for the getLogout action. This means any already signed-in user attempting to access the login or registration endpoints defined in this controller will be redirected to the home page because it doesn’t make any sense for them to register or login anew. If you have a look at the RedirectIfAuthenticated class (app/Http/Middleware/RedirectIfAuthenticated.php) you’ll see the handle method is really simple to understand in that it uses Laravel’s built-in authentication capabilities to determine whether the user is already signed in.

Creating Your Own Middleware Solution

As an exercise let’s create a simple route-level middleware solution that sends a message to the Laravel log when invoked. Begin by creating a new middleware skeleton using Artisan’s make:middleware command:

$ php artisan make:middleware RequestLogger
Middleware created successfully.

This command created a new middleware class skeleton named RequestLogger that resides in the directory app/Http/Middleware. The class currently looks like this:

 <?php

 namespace todoparrot\Http\Middleware;

 use Closure;

 class RequestLogger
 {
     public function handle($request, Closure $next)
     {
         return $next($request);
     }
 }

Modify the handle method to log the visitor’s IP address to the Laravel log, and then pass on the request:

public function handle($request, Closure $next)
{
  \Log::info($request->ip());
  return $next($request);
}

It is worth noting an important distinction here; when you return $next($request) you’re instructing Laravel to execute this middleware before the request is processed. If you want to execute middleware after the request has been processed, you should change the handle logic to look like this:

public function handle($request, Closure $next)
{
  $response = $next($request);
  \Log::info($request->ip());
  return $response;
}

Next, open up app/Http/Kernel.php and register the new RequestLogger middleware:

 protected $routeMiddleware = [
'auth' => \todoparrot\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \todoparrot\Http\Middleware\RedirectIfAuthenticated::class,
'iplogger' => \todoparrot\Http\Middleware\RequestLogger::class,
];

Finally, just reference the iplogger alias whenever you’d like to execute the middleware. Just for the sake of demonstration I’ll place the middleware call in the Welcome controller’s constructor:

 class HomeController extends Controller {

   public function __construct()
   {
     $this->middleware('iplogger');
   }

   ...

 }

After referencing the new middleware, reload an endpoint associated with the reference and check your log (storage/logs). If you are working from your local laptop you’ll see a line that looks like this:

1 [2015-06-08 20:52:15] local.INFO: 127.0.0.1

The 127.0.0.1 is your local IP address. If you’re running this code remotely, then you’ll see a more recognizable IP address, such as 123.456.789.000.

Using Middleware Parameters

New to Laravel 5.1 is support for middleware parameters. This is a long-awaited and tremendously useful feature. To illustrate its practicality, suppose you wanted to create a middleware (we’ll call it ProMiddleware) which granted only those users having accumulated a particular number of forum points access to a particular set of controllers.

public function handle($request, Closure $next, $points)
{
    if (! $request->user()->totalPoints() < $points) {
        return \Redirect::route('welcome')
    }

    return $next($request);
}

When defining the middleware-restricted route, you’ll identify the middleware name and the parameter like so:

Route::group(['middleware' => 'pro:10000'], function()
{
    Route::resource('pro', 'ProController');
});

Reference