今天主题是 Hyperf 的中间件。中间件是请求生命周期中的“关卡”,可以在请求到达控制器之前或之后执行逻辑,如权限校验、日志、跨域处理、参数加解密等。今天我们将亲手构建一个完整的 API 保护体系,并用有趣而实用的示例让你彻底掌握中间件。


在这里插入图片描述

今日目标

  1. 理解 Hyperf 中间件的核心概念与三种级别:全局、路由级、注解级。
  2. 编写一个 Token 校验中间件,保护 /api/* 路由,未授权返回 401。
  3. 实现一个跨域中间件,解决前后端分离开发中的 CORS 问题。
  4. 进阶:编写一个参数加解密中间件,自动解密请求体、加密响应体。
  5. 学会使用 Postman 或 curl 对中间件进行全流程测试,并理解中间件执行顺序。

一、环境准备(约 20 分钟)

我们继续使用 hyperf-app 项目,进入 Docker 容器并开启热重启:

cd swoole-course
docker-compose exec swoole bash
cd /var/www/hyperf-app
# 启动热重启模式,方便后续开发
php bin/hyperf.php server:watch

后续修改代码会自动重启 Worker,无需手动操作。


二、知识核心:Hyperf 中间件机制(约 1 小时)

1. 中间件在生命周期中的位置

回顾请求生命周期:Request → 全局中间件 → 路由匹配 → 路由中间件 → 控制器 → 响应。中间件可以在控制器前后执行,甚至可以提前拦截请求并返回响应,阻止后续流程。

2. 三种级别的中间件
类型作用范围定义方式使用场景
全局中间件所有请求config/autoload/middlewares.php 中注册CORS、日志记录、统计
路由中间件特定路由或路由组config/routes.php 中通过 addMiddleware() 绑定权限校验、角色控制
注解中间件单个控制器或方法通过 #[Middleware] 注解应用细粒度控制,如限流
3. 中间件的实现规范

所有中间件必须实现 Psr\Http\Server\MiddlewareInterface,包含 process 方法:

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    // 前置逻辑:可以修改 $request,或直接返回响应跳过后续
    $response = $handler->handle($request); // 调用下一个中间件或控制器
    // 后置逻辑:可以修改 $response
    return $response;
}

关键点$handler->handle($request) 是执行链的下一个节点,如果不调用,后续中间件和控制器都不会执行。


三、实战:构建 API 保护体系(约 2.5 小时)

项目准备:创建需要保护的 API

为了演示,我们在 app/Controller/ 下创建一个 ApiController.php,模拟一些 API 接口:

<?php
namespace App\Controller;

use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;

#[Controller(prefix: '/api')]
class ApiController extends AbstractController
{
    #[RequestMapping(path: 'user', methods: 'get')]
    public function user()
    {
        return ['id' => 1, 'name' => 'Swoole'];
    }

    #[RequestMapping(path: 'secret', methods: 'post')]
    public function secret()
    {
        return ['data' => 'top secret info'];
    }
}

目标:让 /api/* 只有携带合法 Token 的请求才能访问。

实战 1:Token 校验中间件(路由中间件)

第一步:创建中间件类
新建 app/Middleware/ApiTokenMiddleware.php

<?php
namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;

class ApiTokenMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // 从 Header 中获取 Token
        $token = $request->getHeaderLine('Authorization');
        
        // 模拟校验:有效的 Token 为 "Bearer secret-api-token"
        if ($token !== 'Bearer secret-api-token') {
            // 返回 401 响应,使用 Hyperf 的 Response 工厂
            $response = \Hyperf\Utils\ApplicationContext::getContainer()
                ->get(HttpResponse::class);
            return $response->json([
                'code' => 401,
                'message' => 'Unauthorized: Invalid or missing token.'
            ])->withStatus(401);
        }
        
        // Token 合法,将用户信息写入请求属性,方便控制器获取
        $request = $request->withAttribute('user_id', 123);
        
        // 放行到下一个处理程序
        return $handler->handle($request);
    }
}

第二步:将中间件绑定到 /api 路由组
打开 config/routes.php,添加路由组并应用中间件:

<?php
use Hyperf\HttpServer\Router\Router;

// 定义 /api 前缀的路由组,所有在此组内的路由都会经过 ApiTokenMiddleware
Router::addGroup('/api', function () {
    Router::get('/user', [App\Controller\ApiController::class, 'user']);
    Router::post('/secret', [App\Controller\ApiController::class, 'secret']);
}, ['middleware' => [App\Middleware\ApiTokenMiddleware::class]]);

说明addGroup 的第三个参数可以传递中间件数组。你也可以在单个路由上用 Router::get(...)->middleware(...) 方式绑定。

测试

  • 无 Token 访问:
    curl http://localhost:9501/api/user
    
    返回 {"code":401,"message":"Unauthorized: Invalid or missing token."},状态码 401。
  • 携带合法 Token:
    curl -H "Authorization: Bearer secret-api-token" http://localhost:9501/api/user
    
    正常返回 {"id":1,"name":"Swoole"}
  • 携带非法 Token:
    curl -H "Authorization: Bearer wrong-token" http://localhost:9501/api/user
    
    返回 401。

进阶:在 ApiController 中获取传递的 user_id

public function user()
{
    $userId = $this->request->getAttribute('user_id');
    return ['id' => $userId, 'name' => 'Swoole'];
}

这展示了中间件向控制器传递数据的标准方式。


实战 2:跨域中间件(全局中间件)

在前后端分离开发中,跨域请求默认被浏览器拦截。我们可以编写一个全局中间件,统一添加 CORS 头。

创建 app/Middleware/CorsMiddleware.php

<?php
namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class CorsMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // 对于 OPTIONS 预检请求,直接返回 200 并附加头信息
        if ($request->getMethod() === 'OPTIONS') {
            $response = \Hyperf\Utils\ApplicationContext::getContainer()
                ->get(\Hyperf\HttpServer\Contract\ResponseInterface::class)
                ->withStatus(200);
        } else {
            $response = $handler->handle($request);
        }
        
        // 统一添加 CORS 头
        return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    }
}

注册为全局中间件
打开 config/autoload/middlewares.php(如果没有这个文件则创建):

<?php
return [
    'http' => [
        \App\Middleware\CorsMiddleware::class,
    ],
];

现在所有请求的响应都会带上 CORS 头。你可以用浏览器控制台测试跨域请求,或者用 curl 查看响应头:

curl -v http://localhost:9501/api/user

会看到 Access-Control-Allow-Origin: * 等头。


实战 3:参数加解密中间件(注解中间件)

为增加趣味性,我们实现一个简单的“前端加密请求体,后端自动解密;后端加密响应体,前端解密”的中间件。我们定义两个注解:@DecryptRequest@EncryptResponse

第一步:创建注解类
新建 app/Annotation/DecryptRequest.php

<?php
namespace App\Annotation;

use Hyperf\Di\Annotation\AbstractAnnotation;

#[Attribute(Attribute::TARGET_METHOD)]
class DecryptRequest extends AbstractAnnotation {}

同理创建 app/Annotation/EncryptResponse.php

第二步:实现解密中间件
创建 app/Middleware/DecryptRequestMiddleware.php

<?php
namespace App\Middleware;

use App\Annotation\DecryptRequest;
use Hyperf\Di\Annotation\AnnotationCollector;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Hyperf\HttpServer\Router\Dispatched;

class DecryptRequestMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // 获取匹配到的路由信息
        $dispatched = $request->getAttribute(Dispatched::class);
        if ($dispatched && $dispatched->handler?->callback) {
            $callback = $dispatched->handler->callback;
            // 如果控制器方法上标记了 DecryptRequest 注解
            if (is_array($callback) && count($callback) === 2) {
                [$class, $method] = $callback;
                $annotations = AnnotationCollector::getClassMethodAnnotation($class, $method);
                if (isset($annotations[DecryptRequest::class])) {
                    // 读取加密的请求体(假设前端用 base64 编码)
                    $body = (string) $request->getBody();
                    $decoded = base64_decode($body, true);
                    if ($decoded !== false) {
                        // 构造新的请求体,用解密后的数据替换
                        $stream = new \Hyperf\HttpMessage\Stream\SwooleStream($decoded);
                        $request = $request->withBody($stream);
                        // 重新解析 POST 参数(如果需要)
                        $request = $request->withParsedBody(json_decode($decoded, true) ?? []);
                    }
                }
            }
        }
        return $handler->handle($request);
    }
}

注意:这里我们通过 AnnotationCollector 获取方法注解,并在中间件中动态判断。因为注解中间件需要绑定到类或方法上,我们可以直接在注解中携带中间件类名。更简单的方式是使用 Hyperf 的 #[Middleware(DecryptRequestMiddleware::class)] 注解,不过为了展示注解中间件,我们采用方案:定义一个注解,然后在全局中间件中监听该注解。这里为了清晰,我们采用官方推荐的注解中间件方式:直接在控制器方法上使用 #[Middleware] 注解引入中间件。

简化版注解中间件:Hyperf 允许你在控制器方法上直接指定中间件,而不需要自定义注解:

use Hyperf\HttpServer\Annotation\Middleware;
use App\Middleware\DecryptRequestMiddleware;
use App\Middleware\EncryptResponseMiddleware;

#[Controller(prefix: '/api')]
class ApiController extends AbstractController
{
    #[RequestMapping(path: 'secure', methods: 'post')]
    #[Middleware(DecryptRequestMiddleware::class)]
    #[Middleware(EncryptResponseMiddleware::class)]
    public function secure()
    {
        $data = $this->request->all();
        return ['received' => $data, 'extra' => 'secret'];
    }
}

我们来实现这两个中间件:

DecryptRequestMiddleware(简化,直接用于注解):

<?php
namespace App\Middleware;

use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;

class DecryptRequestMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $body = (string) $request->getBody();
        // 假设加密内容为 base64 编码的 JSON
        $decoded = base64_decode($body, true);
        if ($decoded) {
            $stream = new SwooleStream($decoded);
            $request = $request->withBody($stream);
            $request = $request->withParsedBody(json_decode($decoded, true) ?? []);
        }
        return $handler->handle($request);
    }
}

EncryptResponseMiddleware

<?php
namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Hyperf\HttpMessage\Stream\SwooleStream;

class EncryptResponseMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $response = $handler->handle($request);
        $body = (string) $response->getBody();
        // 将响应体 base64 加密
        $encrypted = base64_encode($body);
        return $response->withBody(new SwooleStream($encrypted))
            ->withHeader('Content-Type', 'text/plain'); // 改变类型
    }
}

测试加密中间件

# 发送加密的请求体(原始 JSON 的 base64)
curl -X POST http://localhost:9501/api/secure -d $(echo -n '{"username":"admin"}' | base64)

返回的响应是加密后的 base64,解码后可见原始 JSON。


四、成果测试与执行顺序观察(约 1 小时)

1. Token 中间件测试清单
场景测试命令预期结果
无 Tokencurl /api/user401 JSON 错误
错误 Tokencurl -H "Authorization: Bearer wrong" /api/user401
正确 Tokencurl -H "Authorization: Bearer secret-api-token" /api/user200,返回用户数据
携带 Token 但访问非保护路由curl -H "Authorization: Bearer secret-api-token" /health200,正常(不应该被拦截)
2. CORS 中间件测试
curl -v http://localhost:9501/api/user
# 查看响应头是否存在 Access-Control-Allow-Origin 等

也可用浏览器打开一个前端页面,用 fetch 请求跨域验证。

3. 参数加解密中间件测试
  • 正常加密请求 + 解密响应:
    # 加密请求
    PLAIN='{"action":"test"}'
    ENC=$(echo -n $PLAIN | base64)
    RESP=$(curl -s -X POST -d "$ENC" http://localhost:9501/api/secure)
    echo "响应密文: $RESP"
    echo "解密后响应: $(echo $RESP | base64 -d)"
    
  • 未加密请求(直接发送 JSON)看中间件是否报错,应该不会被解密,但可能会因为 json_decode 失败而返回空数组。
4. 中间件执行顺序

当一个方法上应用了多个中间件(全局 CorsMiddleware -> 路由 ApiTokenMiddleware -> 注解 DecryptRequestMiddleware -> 注解 EncryptResponseMiddleware),执行顺序为:

  • 前置:全局 → 路由组 → 注解(按声明顺序)的前置部分
  • 后置:注解 → 路由组 → 全局的后置部分

你可以通过在各个中间件的 process 方法前后打印日志来验证。例如在 CorsMiddleware 中添加 echo "Cors Before\n"; ... $response = $handler->handle($request); echo "Cors After\n";,启动热重启,发送请求,查看终端输出顺序。


五、今日作业与学习产出

  1. 提交代码:将 ApiTokenMiddlewareCorsMiddleware、加解密中间件以及路由配置提交到 Git。
  2. 学习笔记:绘制中间件执行链的流程图,标注全局、路由、注解中间件的注册位置和调用顺序。
  3. 进阶任务
    • 为 Token 中间件增加路径排除功能,比如 /api/public 不需要 Token(可在中间件内判断 $request->getUri()->getPath())。
    • 实现一个限流中间件(基于 Redis 计数),并在某个接口上注解使用,体验中间件的复用性。
  4. 思考题:如果我们想在中间件中异步写日志(使用协程),但中间件的 process 方法必须返回 Response,能否在 handler->handle() 之后 go 一个协程做日志写入?这样是否会阻塞?答案是可以,因为协程不会阻塞,但要注意上下文问题。尝试写一个日志中间件,异步记录请求日志。

通过今天的学习,你已经掌握了 Hyperf 中间件的精髓,能灵活运用它来构建安全的 API 网关层。明天我们将继续深化,使用验证器和异常处理器让 API 更加健壮。

更多推荐