中间件

中间件对象使您能够以可重用、可组合的层来“包装”应用程序,这些层可以处理请求,或构建响应逻辑。从视觉上看,您的应用程序最终位于中心,中间件像洋葱一样包裹在应用程序周围。在这里,我们可以看到一个用路由、资产、异常处理和 CORS 标头中间件包裹的应用程序。

../_images/middleware-setup.png

当应用程序处理请求时,它从最外面的中间件进入。每个中间件都可以将请求/响应委托给下一层,或者返回一个响应。返回响应会阻止更低层看到请求。例如,在开发过程中,AssetMiddleware 处理对插件图像的请求。

../_images/middleware-request.png

如果没有中间件采取行动来处理请求,则会找到一个控制器并调用其操作,或者会引发异常,从而生成一个错误页面。

中间件是 CakePHP 中新的 HTTP 堆栈的一部分,该堆栈利用 PSR-7 请求和响应接口。CakePHP 还支持 PSR-15 标准的服务器请求处理程序,因此您可以使用 Packagist 上提供的任何 PSR-15 兼容中间件。

CakePHP 中的中间件

CakePHP 提供了一些中间件来处理 Web 应用程序中的常见任务。

  • Cake\Error\Middleware\ErrorHandlerMiddleware 会捕获来自包装中间件的异常,并使用 错误和异常处理 异常处理程序呈现错误页面。

  • Cake\Routing\AssetMiddleware 检查请求是否引用主题或插件资产文件,例如存储在插件的 webroot 文件夹或主题的相应文件夹中的 CSS、JavaScript 或图像文件。

  • Cake\Routing\Middleware\RoutingMiddleware 使用 Router 解析传入的 URL 并将路由参数分配给请求。

  • Cake\I18n\Middleware\LocaleSelectorMiddleware 使能从浏览器发送的 Accept-Language 标头自动切换语言。

  • Cake\Http\Middleware\EncryptedCookieMiddleware 使您能够在需要操作包含混淆数据的 Cookie 时,操作加密的 Cookie。

  • Cake\Http\Middleware\BodyParserMiddleware 允许您根据 Content-Type 标头解码 JSON、XML 和其他编码的请求正文。

  • CakeHttpMiddlewareHttpsEnforcerMiddleware 要求使用 HTTPS。

  • CakeHttpMiddlewareCsrfProtectionMiddleware 为您的应用程序添加基于双重提交 Cookie 的 CSRF 保护。

  • CakeHttpMiddlewareSessionCsrfProtectionMiddleware 为您的应用程序添加基于会话的 CSRF 保护。

  • CakeHttpMiddlewareCspMiddleware 简化了将内容安全策略标头添加到应用程序的过程。

  • CakeHttpMiddlewareSecurityHeadersMiddleware 使您能够将与安全相关的标头(如 X-Frame-Options)添加到响应。

使用中间件

可以将中间件全局应用于您的应用程序,应用于各个路由范围,或者应用于特定的控制器。

要将中间件应用于所有请求,请使用 middleware 方法,该方法位于 App\Application 类中。应用程序的 middleware 挂钩方法将在请求处理开始时被调用,您可以使用 MiddlewareQueue 对象来附加中间件。

namespace App;

use Cake\Http\BaseApplication;
use Cake\Http\MiddlewareQueue;
use Cake\Error\Middleware\ErrorHandlerMiddleware;

class Application extends BaseApplication
{
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        // Bind the error handler into the middleware queue.
        $middlewareQueue->add(new ErrorHandlerMiddleware());

        // Add middleware by classname.
        // As of 4.5.0 classname middleware are optionally resolved
        // using the DI container. If the class is not found in the
        // container then an instance is created by the middleware queue.
        $middlewareQueue->add(UserRateLimiting::class);

        return $middlewareQueue;
    }
}

除了添加到 MiddlewareQueue 的末尾之外,您还可以执行各种操作。

$layer = new \App\Middleware\CustomMiddleware;

// Added middleware will be last in line.
$middlewareQueue->add($layer);

// Prepended middleware will be first in line.
$middlewareQueue->prepend($layer);

// Insert in a specific slot. If the slot is out of
// bounds, it will be added to the end.
$middlewareQueue->insertAt(2, $layer);

// Insert before another middleware.
// If the named class cannot be found,
// an exception will be raised.
$middlewareQueue->insertBefore(
    'Cake\Error\Middleware\ErrorHandlerMiddleware',
    $layer
);

// Insert after another middleware.
// If the named class cannot be found, the
// middleware will added to the end.
$middlewareQueue->insertAfter(
    'Cake\Error\Middleware\ErrorHandlerMiddleware',
    $layer
);

如果您的中间件仅适用于部分路由或单个控制器,您可以使用 路由范围中间件控制器中间件

从插件添加中间件

插件可以使用它们的 middleware 挂钩方法将它们拥有的任何中间件应用于应用程序的中间件队列。

// in plugins/ContactManager/src/Plugin.php
namespace ContactManager;

use Cake\Core\BasePlugin;
use Cake\Http\MiddlewareQueue;
use ContactManager\Middleware\ContactManagerContextMiddleware;

class Plugin extends BasePlugin
{
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue->add(new ContactManagerContextMiddleware());

        return $middlewareQueue;
    }
}

创建中间件

中间件可以实现为匿名函数(闭包),也可以实现为扩展 Psr\Http\Server\MiddlewareInterface 的类。虽然闭包适用于较小的任务,但它们使测试变得更加困难,并且会创建复杂的 Application 类。CakePHP 中的中间件类具有一些约定。

  • 中间件类文件应放在 src/Middleware 中。例如:src/Middleware/CorsMiddleware.php

  • 中间件类应以 Middleware 结尾。例如:LinkMiddleware

  • 中间件必须实现 Psr\Http\Server\MiddlewareInterface

中间件可以通过调用 $handler->handle() 或创建自己的响应来返回响应。我们可以在简单的中间件中看到这两种选项。

// In src/Middleware/TrackingCookieMiddleware.php
namespace App\Middleware;

use Cake\Http\Cookie\Cookie;
use Cake\I18n\Time;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;

class TrackingCookieMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface
    {
        // Calling $handler->handle() delegates control to the *next* middleware
        // In your application's queue.
        $response = $handler->handle($request);

        if (!$request->getCookie('landing_page')) {
            $expiry = new Time('+ 1 year');
            $response = $response->withCookie(new Cookie(
                'landing_page',
                $request->getRequestTarget(),
                $expiry
            ));
        }

        return $response;
    }
}

现在我们已经创建了一个非常简单的中间件,让我们将它附加到我们的应用程序。

// In src/Application.php
namespace App;

use App\Middleware\TrackingCookieMiddleware;
use Cake\Http\MiddlewareQueue;

class Application
{
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        // Add your simple middleware onto the queue
        $middlewareQueue->add(new TrackingCookieMiddleware());

        // Add some more middleware onto the queue

        return $middlewareQueue;
    }
}

路由中间件

路由中间件负责应用应用程序的路由并解析请求将要访问的插件、控制器和操作。

// In Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    // ...
    $middlewareQueue->add(new RoutingMiddleware($this));
}

正文解析中间件

如果您的应用程序接受 JSON、XML 或其他编码的请求正文,BodyParserMiddleware 将使您能够将这些请求解码为可通过 $request->getParsedData()$request->getData() 获取的数组。默认情况下,只有 json 正文会被解析,但可以通过选项启用 XML 解析。您还可以定义自己的解析器。

use Cake\Http\Middleware\BodyParserMiddleware;

// only JSON will be parsed.
$bodies = new BodyParserMiddleware();

// Enable XML parsing
$bodies = new BodyParserMiddleware(['xml' => true]);

// Disable JSON parsing
$bodies = new BodyParserMiddleware(['json' => false]);

// Add your own parser matching content-type header values
// to the callable that can parse them.
$bodies = new BodyParserMiddleware();
$bodies->addParser(['text/csv'], function ($body, $request) {
    // Use a CSV parsing library.
    return Csv::parse($body);
});