【基于 Swoole+Hyperf 的微服务实战】 第二周·周二:Hyperf 的中间件

📅 2026/8/25 15:48:02
【基于 Swoole+Hyperf 的微服务实战】 第二周·周二:Hyperf 的中间件
今天主题是 Hyperf 的中间件。中间件是请求生命周期中的“关卡”可以在请求到达控制器之前或之后执行逻辑如权限校验、日志、跨域处理、参数加解密等。今天我们将亲手构建一个完整的 API 保护体系并用有趣而实用的示例让你彻底掌握中间件。今日目标理解 Hyperf 中间件的核心概念与三种级别全局、路由级、注解级。编写一个Token 校验中间件保护/api/*路由未授权返回 401。实现一个跨域中间件解决前后端分离开发中的 CORS 问题。进阶编写一个参数加解密中间件自动解密请求体、加密响应体。学会使用 Postman 或 curl 对中间件进行全流程测试并理解中间件执行顺序。一、环境准备约 20 分钟我们继续使用hyperf-app项目进入 Docker 容器并开启热重启cdswoole-coursedocker-composeexecswoolebashcd/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方法publicfunctionprocess(ServerRequestInterface$request,RequestHandlerInterface$handler):ResponseInterface{// 前置逻辑可以修改 $request或直接返回响应跳过后续$response$handler-handle($request);// 调用下一个中间件或控制器// 后置逻辑可以修改 $responsereturn$response;}关键点$handler-handle($request)是执行链的下一个节点如果不调用后续中间件和控制器都不会执行。三、实战构建 API 保护体系约 2.5 小时项目准备创建需要保护的 API为了演示我们在app/Controller/下创建一个ApiController.php模拟一些 API 接口?phpnamespaceApp\Controller;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\RequestMapping;#[Controller(prefix:/api)]classApiControllerextendsAbstractController{#[RequestMapping(path:user,methods:get)]publicfunctionuser(){return[id1,nameSwoole];}#[RequestMapping(path:secret,methods:post)]publicfunctionsecret(){return[datatop secret info];}}目标让/api/*只有携带合法 Token 的请求才能访问。实战 1Token 校验中间件路由中间件第一步创建中间件类新建app/Middleware/ApiTokenMiddleware.php?phpnamespaceApp\Middleware;usePsr\Http\Message\ResponseInterface;usePsr\Http\Message\ServerRequestInterface;usePsr\Http\Server\MiddlewareInterface;usePsr\Http\Server\RequestHandlerInterface;useHyperf\HttpServer\Contract\ResponseInterfaceasHttpResponse;classApiTokenMiddlewareimplementsMiddlewareInterface{publicfunctionprocess(ServerRequestInterface$request,RequestHandlerInterface$handler):ResponseInterface{// 从 Header 中获取 Token$token$request-getHeaderLine(Authorization);// 模拟校验有效的 Token 为 Bearer secret-api-tokenif($token!Bearer secret-api-token){// 返回 401 响应使用 Hyperf 的 Response 工厂$response\Hyperf\Utils\ApplicationContext::getContainer()-get(HttpResponse::class);return$response-json([code401,messageUnauthorized: Invalid or missing token.])-withStatus(401);}// Token 合法将用户信息写入请求属性方便控制器获取$request$request-withAttribute(user_id,123);// 放行到下一个处理程序return$handler-handle($request);}}第二步将中间件绑定到/api路由组打开config/routes.php添加路由组并应用中间件?phpuseHyperf\HttpServer\Router\Router;// 定义 /api 前缀的路由组所有在此组内的路由都会经过 ApiTokenMiddlewareRouter::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 访问curlhttp://localhost:9501/api/user返回{code:401,message:Unauthorized: Invalid or missing token.}状态码 401。携带合法 Tokencurl-HAuthorization: Bearer secret-api-tokenhttp://localhost:9501/api/user正常返回{id:1,name:Swoole}。携带非法 Tokencurl-HAuthorization: Bearer wrong-tokenhttp://localhost:9501/api/user返回 401。进阶在ApiController中获取传递的user_idpublicfunctionuser(){$userId$this-request-getAttribute(user_id);return[id$userId,nameSwoole];}这展示了中间件向控制器传递数据的标准方式。实战 2跨域中间件全局中间件在前后端分离开发中跨域请求默认被浏览器拦截。我们可以编写一个全局中间件统一添加 CORS 头。创建app/Middleware/CorsMiddleware.php?phpnamespaceApp\Middleware;usePsr\Http\Message\ResponseInterface;usePsr\Http\Message\ServerRequestInterface;usePsr\Http\Server\MiddlewareInterface;usePsr\Http\Server\RequestHandlerInterface;classCorsMiddlewareimplementsMiddlewareInterface{publicfunctionprocess(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如果没有这个文件则创建?phpreturn[http[\App\Middleware\CorsMiddleware::class,],];现在所有请求的响应都会带上 CORS 头。你可以用浏览器控制台测试跨域请求或者用 curl 查看响应头curl-vhttp://localhost:9501/api/user会看到Access-Control-Allow-Origin: *等头。实战 3参数加解密中间件注解中间件为增加趣味性我们实现一个简单的“前端加密请求体后端自动解密后端加密响应体前端解密”的中间件。我们定义两个注解DecryptRequest和EncryptResponse。第一步创建注解类新建app/Annotation/DecryptRequest.php?phpnamespaceApp\Annotation;useHyperf\Di\Annotation\AbstractAnnotation;#[Attribute(Attribute::TARGET_METHOD)]classDecryptRequestextendsAbstractAnnotation{}同理创建app/Annotation/EncryptResponse.php。第二步实现解密中间件创建app/Middleware/DecryptRequestMiddleware.php?phpnamespaceApp\Middleware;useApp\Annotation\DecryptRequest;useHyperf\Di\Annotation\AnnotationCollector;usePsr\Http\Message\ResponseInterface;usePsr\Http\Message\ServerRequestInterface;usePsr\Http\Server\MiddlewareInterface;usePsr\Http\Server\RequestHandlerInterface;useHyperf\HttpServer\Router\Dispatched;classDecryptRequestMiddlewareimplementsMiddlewareInterface{publicfunctionprocess(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;$annotationsAnnotationCollector::getClassMethodAnnotation($class,$method);if(isset($annotations[DecryptRequest::class])){// 读取加密的请求体假设前端用 base64 编码$body(string)$request-getBody();$decodedbase64_decode($body,true);if($decoded!false){// 构造新的请求体用解密后的数据替换$streamnew\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 允许你在控制器方法上直接指定中间件而不需要自定义注解useHyperf\HttpServer\Annotation\Middleware;useApp\Middleware\DecryptRequestMiddleware;useApp\Middleware\EncryptResponseMiddleware;#[Controller(prefix:/api)]classApiControllerextendsAbstractController{#[RequestMapping(path:secure,methods:post)]#[Middleware(DecryptRequestMiddleware::class)]#[Middleware(EncryptResponseMiddleware::class)]publicfunctionsecure(){$data$this-request-all();return[received$data,extrasecret];}}我们来实现这两个中间件DecryptRequestMiddleware简化直接用于注解?phpnamespaceApp\Middleware;useHyperf\HttpMessage\Stream\SwooleStream;usePsr\Http\Message\ServerRequestInterface;usePsr\Http\Server\MiddlewareInterface;usePsr\Http\Server\RequestHandlerInterface;usePsr\Http\Message\ResponseInterface;classDecryptRequestMiddlewareimplementsMiddlewareInterface{publicfunctionprocess(ServerRequestInterface$request,RequestHandlerInterface$handler):ResponseInterface{$body(string)$request-getBody();// 假设加密内容为 base64 编码的 JSON$decodedbase64_decode($body,true);if($decoded){$streamnewSwooleStream($decoded);$request$request-withBody($stream);$request$request-withParsedBody(json_decode($decoded,true)??[]);}return$handler-handle($request);}}EncryptResponseMiddleware?phpnamespaceApp\Middleware;usePsr\Http\Message\ResponseInterface;usePsr\Http\Message\ServerRequestInterface;usePsr\Http\Server\MiddlewareInterface;usePsr\Http\Server\RequestHandlerInterface;useHyperf\HttpMessage\Stream\SwooleStream;classEncryptResponseMiddlewareimplementsMiddlewareInterface{publicfunctionprocess(ServerRequestInterface$request,RequestHandlerInterface$handler):ResponseInterface{$response$handler-handle($request);$body(string)$response-getBody();// 将响应体 base64 加密$encryptedbase64_encode($body);return$response-withBody(newSwooleStream($encrypted))-withHeader(Content-Type,text/plain);// 改变类型}}测试加密中间件# 发送加密的请求体原始 JSON 的 base64curl-XPOST 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-vhttp://localhost:9501/api/user# 查看响应头是否存在 Access-Control-Allow-Origin 等也可用浏览器打开一个前端页面用fetch请求跨域验证。3. 参数加解密中间件测试正常加密请求 解密响应# 加密请求PLAIN{action:test}ENC$(echo-n$PLAIN|base64)RESP$(curl-s-XPOST-d$ENChttp://localhost:9501/api/secure)echo响应密文:$RESPecho解密后响应:$(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 计数并在某个接口上注解使用体验中间件的复用性。思考题如果我们想在中间件中异步写日志使用协程但中间件的process方法必须返回 Response能否在handler-handle()之后go一个协程做日志写入这样是否会阻塞答案是可以因为协程不会阻塞但要注意上下文问题。尝试写一个日志中间件异步记录请求日志。通过今天的学习你已经掌握了 Hyperf 中间件的精髓能灵活运用它来构建安全的 API 网关层。明天我们将继续深化使用验证器和异常处理器让 API 更加健壮。