中间件
Viswoole 中间件采用洋葱模型(Onion Model)设计,提供 HTTP 请求的拦截与处理能力。中间件按注册顺序依次包裹请求,形成层层嵌套的执行链。
工作原理
请求进入 → Middleware1 → Middleware2 → Middleware3 → 核心处理
↓
响应返回 ← Middleware1 ← Middleware2 ← Middleware3 ← 核心处理每个中间件的 process() 方法接收一个 $handler 闭包,调用 $handler() 将控制权传递给下一个中间件。
中间件接口
所有中间件必须实现 MiddlewareInterface 接口:
namespace Viswoole\Core\Contract;
interface MiddlewareInterface
{
/**
* 执行中间件逻辑
*
* @param Closure $handler 下一个中间件的处理闭包
* @return mixed 处理结果
*/
public function process(Closure $handler): mixed;
}创建中间件
基础中间件
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;
}
}带依赖注入的中间件
namespace App\Middleware;
use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\Core\Coroutine\Context;
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 并非中间件预处理接口,而是容器在解析参数时调用的自定义注入契约。框架内置的 InjectGet、InjectPost、InjectHeader 等自动注入属性均实现该接口。
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 请求体中取值并校验空值:
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 预检请求:
use Viswoole\Core\Middlewares\AllowCrossDomain;
// 自动添加响应头:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Headers: *
// Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
// Access-Control-Max-Age: 86400
// OPTIONS 预检请求直接返回,其他请求放行自定义跨域配置:
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();
}
}注册中间件
全局中间件
在懒加载配置文件 config/lazy/middleware.php 中通过 Middleware 门面注册全局中间件,对所有服务器与路由生效:
// config/lazy/middleware.php
use App\Middleware\AuthMiddleware;
use App\Middleware\RequestLogMiddleware;
use Viswoole\Core\Facade\Middleware;
use Viswoole\Core\Middlewares\AllowCrossDomain;
// 注册全局中间件(对所有路由生效)
Middleware::register(AllowCrossDomain::class);
Middleware::register(RequestLogMiddleware::class);
Middleware::register(AuthMiddleware::class);也可以为指定服务器注册中间件,只需传入第二个参数 $server(服务器名称):
Middleware::register(RequestLogMiddleware::class, 'http');路由级中间件
为特定路由或路由组指定中间件,通过 Router 门面的 setMiddlewares() 方法追加:
use Viswoole\Router\Facade\Router;
// 单个路由
Router::get('/api/user/profile', [UserController::class, 'profile'])
->setMiddlewares([AuthMiddleware::class]);
// 路由组(group 方法需指定路由组 ID)
Router::group('/api/v1', function () {
Router::get('users', [UserController::class, 'index']);
Router::post('orders', [OrderController::class, 'create']);
}, 'api_v1')->setMiddlewares([AuthMiddleware::class, RateLimitMiddleware::class]);控制器级中间件
通过注解路由为控制器或方法指定中间件,Controller 注解作用于类(路由组),RouteMapping 注解作用于方法:
use Viswoole\Core\Coroutine\Context;
use Viswoole\Router\Annotation\Controller;
use Viswoole\Router\Annotation\RouteMapping;
// 控制器级中间件(对该控制器下所有路由生效)
#[Controller(prefix: '/api', middlewares: [AuthMiddleware::class])]
class UserController
{
// 方法级中间件(仅对该方法生效)
#[RouteMapping('profile', middlewares: [AuthMiddleware::class])]
public function profile(): array
{
// 已经过 AuthMiddleware 验证
return Context::get('current_user');
}
// 未声明中间件的方法则不需要认证
#[RouteMapping('public/info')]
public function publicInfo(): array
{
return ['version' => '1.0.0'];
}
}常用中间件模式
1. IP 白名单
use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\HttpServer\Contract\RequestInterface;
use Viswoole\HttpServer\Contract\ResponseInterface;
class IpWhitelistMiddleware implements MiddlewareInterface
{
private array $allowedIps;
public function __construct(
protected RequestInterface $request,
protected ResponseInterface $response,
array $allowedIps = []
) {
$this->allowedIps = $allowedIps ?: config('ip_whitelist', []);
}
public function process(Closure $handler): mixed
{
$clientIp = $this->request->ip();
if (!in_array($clientIp, $this->allowedIps)) {
return $this->response->status(403)->json([
'code' => 403,
'msg' => 'IP不在白名单中'
]);
}
return $handler();
}
}2. 请求频率限制
use Closure;
use Viswoole\Cache\Facade\Cache;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\HttpServer\Contract\RequestInterface;
use Viswoole\HttpServer\Contract\ResponseInterface;
class RateLimitMiddleware implements MiddlewareInterface
{
public function __construct(
protected RequestInterface $request,
protected ResponseInterface $response
) {}
public function process(Closure $handler): mixed
{
$key = 'rate_limit:' . $this->request->ip();
$maxAttempts = 60; // 最大请求数
$decaySeconds = 60; // 时间窗口(秒)
$current = Cache::get($key, 0);
if ($current >= $maxAttempts) {
return $this->response->status(429)->json([
'code' => 429,
'msg' => '请求过于频繁,请稍后再试',
])->header('Retry-After', (string)$decaySeconds);
}
Cache::set($key, $current + 1, $decaySeconds);
return $handler();
}
}3. 请求体校验
use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\Core\Coroutine\Context;
use Viswoole\HttpServer\Contract\RequestInterface;
use Viswoole\HttpServer\Contract\ResponseInterface;
class ValidateJsonBodyMiddleware implements MiddlewareInterface
{
public function __construct(
protected RequestInterface $request,
protected ResponseInterface $response
) {}
public function process(Closure $handler): mixed
{
$contentType = (string)$this->request->getHeader('Content-Type');
if (!str_contains($contentType, 'application/json')) {
return $this->response->status(400)->json([
'code' => 400,
'msg' => '仅支持 JSON 请求格式',
]);
}
$body = $this->request->getContent();
if (empty($body)) {
return $this->response->status(400)->json([
'code' => 400,
'msg' => '请求体不能为空',
]);
}
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->response->status(400)->json([
'code' => 400,
'msg' => 'JSON 格式错误: ' . json_last_error_msg(),
]);
}
// 将解析后的数据存入上下文
Context::set('parsed_body', $data);
return $handler();
}
}4. 日志追踪
use Closure;
use Viswoole\Core\Contract\MiddlewareInterface;
use Viswoole\Core\Coroutine\Context;
use Viswoole\HttpServer\Contract\RequestInterface;
use Viswoole\HttpServer\Contract\ResponseInterface;
class TraceIdMiddleware implements MiddlewareInterface
{
public function __construct(
protected RequestInterface $request,
protected ResponseInterface $response
) {}
public function process(Closure $handler): mixed
{
// 生成或获取请求追踪 ID
$traceId = $this->request->getHeader('X-Trace-Id')
?: uniqid('trace_', more_entropy: true);
// 注入到响应头
$this->response->header('X-Trace-Id', $traceId);
// 存入协程上下文,供日志等模块使用
Context::set('trace_id', $traceId);
return $handler();
}
}执行顺序
中间件按照注册顺序从外到内包裹请求处理:
'middleware' => [A, B, C]执行流程:
A::process() 开始
B::process() 开始
C::process() 开始
核心处理(Controller Action)
C::process() 结束
B::process() 结束
A::process() 结束关键点:
$handler()之前的代码在请求到达核心处理之前执行$handler()之后的代码在响应返回之前执行- 中间件可以在前置阶段直接返回响应,阻止后续执行
