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

今日目标
- 理解 Hyperf 中间件的核心概念与三种级别:全局、路由级、注解级。
- 编写一个 Token 校验中间件,保护
/api/*路由,未授权返回 401。 - 实现一个跨域中间件,解决前后端分离开发中的 CORS 问题。
- 进阶:编写一个参数加解密中间件,自动解密请求体、加密响应体。
- 学会使用 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:
返回 401。curl -H "Authorization: Bearer wrong-token" http://localhost:9501/api/user
进阶:在 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 中间件测试清单
| 场景 | 测试命令 | 预期结果 |
|---|---|---|
| 无 Token | curl /api/user | 401 JSON 错误 |
| 错误 Token | curl -H "Authorization: Bearer wrong" /api/user | 401 |
| 正确 Token | curl -H "Authorization: Bearer secret-api-token" /api/user | 200,返回用户数据 |
| 携带 Token 但访问非保护路由 | curl -H "Authorization: Bearer secret-api-token" /health | 200,正常(不应该被拦截) |
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";,启动热重启,发送请求,查看终端输出顺序。
五、今日作业与学习产出
- 提交代码:将
ApiTokenMiddleware、CorsMiddleware、加解密中间件以及路由配置提交到 Git。 - 学习笔记:绘制中间件执行链的流程图,标注全局、路由、注解中间件的注册位置和调用顺序。
- 进阶任务:
- 为 Token 中间件增加路径排除功能,比如
/api/public不需要 Token(可在中间件内判断$request->getUri()->getPath())。 - 实现一个限流中间件(基于 Redis 计数),并在某个接口上注解使用,体验中间件的复用性。
- 为 Token 中间件增加路径排除功能,比如
- 思考题:如果我们想在中间件中异步写日志(使用协程),但中间件的
process方法必须返回 Response,能否在handler->handle()之后go一个协程做日志写入?这样是否会阻塞?答案是可以,因为协程不会阻塞,但要注意上下文问题。尝试写一个日志中间件,异步记录请求日志。
通过今天的学习,你已经掌握了 Hyperf 中间件的精髓,能灵活运用它来构建安全的 API 网关层。明天我们将继续深化,使用验证器和异常处理器让 API 更加健壮。
更多推荐
所有评论(0)