ThinkPHP 8 中间件深度实战:用管道模式构建认证、限流与日志的全链路拦截体系

2026-06-30 0 650

一个成熟的Web应用,请求从抵达路由到最终返回响应之间,要经过身份校验、参数清洗、频率控制、日志记录等一系列横切关注点。如果把这些逻辑全部散落在控制器里,代码复用性基本为零。ThinkPHP 8 的中间件机制正是为此而生——它采用标准的管道模式,让你可以把每一道拦截逻辑封装成独立的“切面”,按需装配到不同的路由组上。这篇文章会从零开始,带你实现认证、限流、日志三个完整的中间件,并展示如何将它们组合成一个可配置的全链路拦截体系。

一、中间件在ThinkPHP 8中的位置

一次完整的请求周期是这样的:请求进入应用 → 全局中间件 → 应用中间件 → 路由匹配 → 路由中间件 → 控制器执行 → 响应返回。每一步中间件都可以拿到请求和响应对象,并在执行下一个中间件前后插入自己的逻辑。这个“包裹洋葱”的模型在框架内部通过 thinkmiddleware 下的管道类实现,我们自定义的中间件只需要实现一个 handle 方法。

ThinkPHP 8 的全局中间件配置在 app/middleware.php,应用级中间件在 app/应用名/middleware.php,也可以在路由定义中直接用 ->middleware() 链式绑定。本文重点讲后两种,因为实际业务中很少所有接口都用同一个大杂烩中间件,分组绑定才是主流。

二、起步:一个基础的请求计时中间件

我们先写一个最简单的中间件来熟悉流程。这个中间件记录每个请求的处理耗时,并添加到响应头里。

app/common/middleware 下创建 RequestTimer.php(多应用项目建议放公共目录):

<?php
namespace appcommonmiddleware;

use thinkRequest;
use thinkResponse;

class RequestTimer
{
    public function handle(Request $request, Closure $next): Response
    {
        $start = microtime(true);
        
        // 执行后续中间件和控制器
        $response = $next($request);
        
        $elapsed = round((microtime(true) - $start) * 1000, 2);
        $response->header(['X-Request-Time' => $elapsed . 'ms']);
        
        return $response;
    }
}

然后在 app/api/route/route.php 里给需要监控的路由组挂上它:

use appcommonmiddlewareRequestTimer;

Route::group('v1', function () {
    Route::get('data', 'DataController/index');
})->middleware(RequestTimer::class);

现在访问接口,响应头里就会出现 X-Request-Time。这个简单的包裹模式,就是后面所有复杂中间件的基础。

三、实战案例一:JWT认证中间件(无状态身份校验)

对于纯API应用,JWT是最常见的认证方式。我们可以把token解析和用户身份注入封装在一个中间件里。

app/api/middleware 下创建 JwtAuth.php

<?php
namespace appapimiddleware;

use thinkRequest;
use thinkResponse;
use FirebaseJWTJWT;
use FirebaseJWTKey;
use FirebaseJWTExpiredException;
use thinkexceptionHttpResponseException;

class JwtAuth
{
    public function handle(Request $request, Closure $next): Response
    {
        $token = $request->header('Authorization');
        if (empty($token)) {
            throw new HttpResponseException(json([
                'code' => 401,
                'message' => '缺少认证令牌',
            ], 401));
        }

        // 去掉 Bearer 前缀
        $token = str_replace('Bearer ', '', $token);

        try {
            $decoded = JWT::decode($token, new Key(config('jwt.secret'), 'HS256'));
            // 将用户ID注入请求对象,供后续使用
            $request->userId = $decoded->uid;
        } catch (ExpiredException $e) {
            throw new HttpResponseException(json([
                'code' => 401,
                'message' => '令牌已过期,请重新登录',
            ], 401));
        } catch (Exception $e) {
            throw new HttpResponseException(json([
                'code' => 401,
                'message' => '无效的认证令牌',
            ], 401));
        }

        return $next($request);
    }
}

这里用 HttpResponseException 直接终止请求并返回401响应,后续的控制器和中间件都不会再执行。认证通过后,$request->userId 里就有了当前用户ID,控制器里直接用 $request->userId 即可,不需要再重复解析token。

绑定到需要登录的路由组:

Route::group('v1', function () {
    Route::get('user/profile', 'UserController/profile');
    Route::post('order/create', 'OrderController/create');
})->middleware([RequestTimer::class, JwtAuth::class]);

注意中间件执行的顺序就是数组的顺序,先计时(包裹在最外层),再认证(内层)。如果认证失败抛出异常,计时器也会记录耗时——这正是我们想要的。

四、实战案例二:基于Redis的接口限流中间件

对外的API需要防刷。我们可以利用Redis的滑动窗口或者固定窗口算法,实现一个简单的限流中间件。

创建 app/api/middleware/RateLimiter.php

<?php
namespace appapimiddleware;

use thinkRequest;
use thinkResponse;
use thinkfacadeCache;
use thinkexceptionHttpResponseException;

class RateLimiter
{
    /**
     * @param int $maxPerMinute 每分钟最大请求数
     */
    public function handle(Request $request, Closure $next, int $maxPerMinute = 60): Response
    {
        $key = 'rate:' . $request->ip() . ':' . $request->pathinfo();
        $current = Cache::get($key, 0);

        if ($current >= $maxPerMinute) {
            throw new HttpResponseException(json([
                'code' => 429,
                'message' => '请求过于频繁,请稍后再试',
            ], 429));
        }

        // 自增计数,有效期设置为60秒
        Cache::set($key, $current + 1, 60);

        return $next($request);
    }
}

这个中间件可以接受一个参数 $maxPerMinute,通过路由绑定时传递:

// 公开接口,限流每分钟30次
Route::group('v1', function () {
    Route::get('news', 'NewsController/index');
})->middleware([RequestTimer::class, RateLimiter::class . ':30']);

中间件类名后面用冒号传递参数,多个参数用逗号分隔。这种参数化的设计让同一个中间件可以服务于不同的限流策略,比写死数字灵活得多。

五、实战案例三:请求与响应日志中间件

调试和审计时需要记录完整的请求和响应信息。我们可以写一个日志中间件,把有用信息存到数据库或文件。

创建 app/api/middleware/RequestLogger.php

<?php
namespace appapimiddleware;

use thinkRequest;
use thinkResponse;
use thinkfacadeLog;

class RequestLogger
{
    public function handle(Request $request, Closure $next): Response
    {
        $startTime = microtime(true);
        $requestData = [
            'method' => $request->method(),
            'url'    => $request->url(true),
            'params' => $request->param(),
            'ip'     => $request->ip(),
            'header' => $request->header('authorization') ? '***' : '',
        ];

        // 执行后续
        $response = $next($request);

        $executionTime = round((microtime(true) - $startTime) * 1000, 2);
        $logData = array_merge($requestData, [
            'status'  => $response->getCode(),
            'time'    => $executionTime . 'ms',
            'date'    => date('Y-m-d H:i:s'),
        ]);

        // 异步写入日志,避免阻塞响应
        Log::info(json_encode($logData, JSON_UNESCAPED_UNICODE));

        return $response;
    }
}

这里把认证头做了脱敏处理,避免token泄入日志文件。日志记录可以进一步通过队列异步写入数据库,但直接用 Log::info 写入文件已经满足大部分场景。

将这个中间件挂到全局作用域,或只放在需要审计的路由组上:

Route::group('v1', function () {
    // 所有v1接口都记日志
})->middleware([RequestTimer::class, RequestLogger::class, RateLimiter::class . ':60']);

六、管道的组合艺术:中间件顺序为何至关重要

从上面的装配可以看出,中间件的顺序决定了请求穿过层层的次序:

  • 最先执行计时器(包裹所有),记录总耗时。
  • 然后是限流器,在认证之前就可以拒绝高频请求,减少不必要的密码验证开销。
  • 接着是日志,此时还没有认证信息,只记录基础请求数据。
  • 最后是认证,认证通过后控制器执行,响应再原路返回。

如果你的日志需要记录用户ID,那就应该把认证中间件放在日志前面。这种排列组合的自由度,正是管道模式相比于写在控制器 __construct 里的最大优势。

七、中间件的注册与全局配置

除了路由绑定,你还可以在应用的 middleware.php 中注册中间件。例如在 app/api/middleware.php 中:

<?php
return [
    appcommonmiddlewareRequestTimer::class,
    appapimiddlewareRequestLogger::class,
];

这样所有进入该应用的请求都会经过计时和日志中间件,而认证和限流仍保留在路由级绑定,因为它们需要选择性使用。这种分层注册的方式让配置更加合理。

八、总结

ThinkPHP 8 的中间件设计干净且强大,它把每一个横切关注点变成了可复用、可组合的小块。本文的三个实战案例覆盖了认证、限流、日志三个最常见的中间件需求,其中的参数传递和顺序控制经验可以直接用于生产环境。当你把业务逻辑中所有“非核心”的部分都抽离成中间件后,控制器会变得极其薄——只负责协调领域模型并返回结果,这才是框架该有的模样。

ThinkPHP 8 中间件深度实战:用管道模式构建认证、限流与日志的全链路拦截体系
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8 中间件深度实战:用管道模式构建认证、限流与日志的全链路拦截体系 https://www.taomawang.com/server/thinkphp/2297.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务