中间件

Viswoole 中间件采用洋葱模型(Onion Model)设计,提供 HTTP 请求的拦截与处理能力。中间件按注册顺序依次包裹请求,形成层层嵌套的执行链。

工作原理

text
请求进入 → Middleware1 → Middleware2 → Middleware3 → 核心处理

响应返回 ← Middleware1 ← Middleware2 ← Middleware3 ← 核心处理

每个中间件的 process() 方法接收一个 $handler 闭包,调用 $handler() 将控制权传递给下一个中间件。

中间件接口

所有中间件必须实现 MiddlewareInterface 接口:

php
namespace Viswoole\Core\Contract;

interface MiddlewareInterface
{
    /**
     * 执行中间件逻辑
     *
     * @param Closure $handler 下一个中间件的处理闭包
     * @return mixed 处理结果
     */
    public function process(Closure $handler): mixed;
}

创建中间件

基础中间件

php
namespace App\Middleware;

use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\HttpServer\Facade\Response;

class RequestLogMiddleware implements MiddlewareInterface
{
    public function process(Closure $handler): mixed
    {
        $start = microtime(true);

        // 前置逻辑:记录请求开始时间
        // ...

        // 调用后续中间件和核心处理
        $response = $handler();

        // 后置逻辑:计算耗时并记录日志
        $duration = round((microtime(true) - $start) * 1000, 2);
        Response::header('X-Response-Time', "{$duration}ms");

        return $response;
    }
}

带依赖注入的中间件

php
namespace App\Middleware;

use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\HttpServer\Contract\RequestInterface;
use Viswoole\HttpServer\Contract\ResponseInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        protected RequestInterface $request,
        protected ResponseInterface $response
    ) {}

    public function process(Closure $handler): mixed
    {
        $token = $this->request->getHeaderLine('Authorization');

        if (empty($token)) {
            return $this->response->json([
                'code' => 401,
                'msg' => '缺少认证令牌'
            ], 401);
        }

        $user = $this->verifyToken($token);

        if (!$user) {
            return $this->response->json([
                'code' => 401,
                'msg' => '认证令牌无效'
            ], 401);
        }

        // 将用户信息存入上下文供后续使用
        Context::set('current_user', $user);

        return $handler();
    }

    private function verifyToken(string $token): ?array
    {
        // Token 验证逻辑...
    }
}

前置注入接口 PreInjectInterface

PreInjectInterface 用于自定义容器参数注入逻辑,详见源码 Viswoole\Core\Contract\PreInjectInterface.php

PreInjectInterface 并非中间件预处理接口,而是容器在解析参数时调用的自定义注入契约。框架内置的 InjectGetInjectPostInjectHeader 等自动注入属性均实现该接口。

php
namespace Viswoole\Core\Contract;

interface PreInjectInterface
{
    /**
     * 自定义参数注入逻辑,容器解析参数时调用
     *
     * @param string $name 当前正在注入的参数名称
     * @param mixed $value 参数默认值,无默认值时为 null
     * @param bool $allowNull 参数是否允许为 null
     * @return mixed 返回实际注入的值
     */
    public function inject(string $name, mixed $value, bool $allowNull): mixed;
}

例如 InjectPost 通过实现 inject() 从 POST 请求体中取值并校验空值:

php
use Viswoole\HttpServer\AutoInject\InjectPost;

class UserController
{
    public function create(#[InjectPost] string $name, #[InjectPost] string $email): array
    {
        // $name 和 $email 已由 InjectPost::inject() 从 POST 请求体注入
        return ['name' => $name, 'email' => $email];
    }
}

如需自定义参数来源(如从特定 Header、Session 等注入),可实现 PreInjectInterface 并以 PHP 8 Attribute 形式标注到参数或属性上。

内置中间件

AllowCrossDomain 跨域中间件

框架内置的 CORS 跨域中间件,自动处理 OPTIONS 预检请求:

php
use Viswoole\Core\Middlewares\AllowCrossDomain;

// 自动添加响应头:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Headers: *

// OPTIONS 预检请求直接返回,其他请求放行

自定义跨域配置

php
namespace App\Middleware;

use Closure;
use Viswoole\Core\Middlewares\AllowCrossDomain;

class CustomCorsMiddleware extends AllowCrossDomain
{
    public function process(Closure $handler): mixed
    {
        if ($this->request->getMethod() === 'OPTIONS') {
            $this->response->setHeaders([
                'Access-Control-Allow-Origin' => env('CORS_ORIGIN', 'https://example.com'),
                'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
                'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
                'Access-Control-Max-Age' => '86400',
            ]);

            return $this->response;
        }

        return $handler();
    }
}

注册中间件

全局中间件

在路由配置文件中注册全局中间件:

php
// config/route.php 或 route/route.php
use App\Middleware\AuthMiddleware;
use App\Middleware\RequestLogMiddleware;
use Viswoole\Core\Middlewares\AllowCrossDomain;

return [
    // 全局中间件(对所有路由生效)
    'middleware' => [
        AllowCrossDomain::class,
        RequestLogMiddleware::class,
    ],

    // 路由定义...
];

路由级中间件

为特定路由或路由组指定中间件:

php
use Viswoole\Router\Route;

// 单个路由
Route::get('/api/user/profile', [UserController::class, 'profile'])
    ->middleware(AuthMiddleware::class);

// 路由组
Route::group('/api/v1', function () {
    Route::get('/users', [UserController::class, 'index']);
    Route::post('/orders', [OrderController::class, 'create']);
})->middleware([AuthMiddleware::class, RateLimitMiddleware::class]);

控制器级中间件

在控制器中定义中间件:

php
class UserController extends Controller
{
    /**
     * 应用到此控制器的中间件
     */
    protected array $middlewares = [
        AuthMiddleware::class,
    ];

    public function profile(): array
    {
        // 已经过 AuthMiddleware 验证
        return Context::get('current_user');
    }

    // 可以为单个方法排除中间件
    public function publicInfo(): array
    {
        // 此方法不需要认证
        return ['version' => '1.0.0'];
    }
}

常用中间件模式

1. IP 白名单

php
class IpWhitelistMiddleware implements MiddlewareInterface
{
    private array $allowedIps;

    public function __construct(array $allowedIps = [])
    {
        $this->allowedIps = $allowedIps ?: config('ip_whitelist', []);
    }

    public function process(Closure $handler): mixed
    {
        $clientIp = request()->getRealIp();

        if (!in_array($clientIp, $this->allowedIps)) {
            return response()->json(['code' => 403, 'msg' => 'IP不在白名单中'], 403);
        }

        return $handler();
    }
}

2. 请求频率限制

php
class RateLimitMiddleware implements MiddlewareInterface
{
    public function process(Closure $handler): mixed
    {
        $key = 'rate_limit:' . request()->getRealIp();
        $maxAttempts = 60;       // 最大请求数
        $decaySeconds = 60;      // 时间窗口(秒)

        $current = Cache::get($key, 0);

        if ($current >= $maxAttempts) {
            return response()->json([
                'code' => 429,
                'msg' => '请求过于频繁,请稍后再试',
            ], 429)->withHeader('Retry-After', $decaySeconds);
        }

        Cache::set($key, $current + 1, $decaySeconds);

        return $handler();
    }
}

3. 请求体校验

php
class ValidateJsonBodyMiddleware implements MiddlewareInterface
{
    public function process(Closure $handler): mixed
    {
        $contentType = request()->getHeaderLine('Content-Type');

        if (!str_contains($contentType, 'application/json')) {
            return response()->json([
                'code' => 400,
                'msg' => '仅支持 JSON 请求格式',
            ], 400);
        }

        $body = request()->getBody()->getContents();

        if (empty($body)) {
            return response()->json([
                'code' => 400,
                'msg' => '请求体不能为空',
            ], 400);
        }

        $data = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return response()->json([
                'code' => 400,
                'msg' => 'JSON 格式错误: ' . json_last_error_msg(),
            ], 400);
        }

        // 将解析后的数据存入上下文
        Context::set('parsed_body', $data);

        return $handler();
    }
}

4. 日志追踪

php
class TraceIdMiddleware implements MiddlewareInterface
{
    public function process(Closure $handler): mixed
    {
        // 生成或获取请求追踪 ID
        $traceId = request()->getHeaderLine('X-Trace-Id')
            ?: uniqid('trace_', more_entropy: true);

        // 注入到响应头
        response()->header('X-Trace-Id', $traceId);

        // 存入协程上下文,供日志等模块使用
        Context::set('trace_id', $traceId);

        return $handler();
    }
}

执行顺序

中间件按照注册顺序从外到内包裹请求处理:

php
'middleware' => [A, B, C]

执行流程:

text
A::process() 开始
  B::process() 开始
    C::process() 开始
      核心处理(Controller Action)
    C::process() 结束
  B::process() 结束
A::process() 结束

关键点:

  • $handler() 之前的代码在请求到达核心处理之前执行
  • $handler() 之后的代码在响应返回之前执行
  • 中间件可以在前置阶段直接返回响应,阻止后续执行