ThinkPHP 8 中间件深度实战:构建权限验证与接口频率限制的全面方案

2026-06-27 0 704

在构建 API 的时候,你一定反复写过那些“每次请求都要检查一遍”的逻辑:判断用户有没有登录、有没有权限访问当前接口、请求频率有没有超标……这些代码散落在控制器方法里,既重复又难维护。ThinkPHP 8 的中间件机制就是为了把这类横切关注点从业务逻辑里剥离出来。今天我们不谈抽象概念,用两个真实可运行的中间件 —— 一个负责权限校验、一个负责接口限流 —— 把这件事彻底讲清楚。

一、动手之前的准备

下面所有操作基于 ThinkPHP 8.0.* 版本。如果你还没有安装,执行这一条命令即可创建一个新项目:

composer create-project topthink/think tp8-middleware-demo

进入项目目录后,确认app目录下有默认的controllermiddleware文件夹(没有就手动创建)。我们还会用到缓存来记录调用次数,所以确保config/cache.php中的默认缓存驱动可用(默认的file就可以)。

为了后续演示,先创建一个简单的控制器,用来返回用户信息:

php think make:controller User

这条命令会在app/controller/User.php生成一个空控制器。我们给它加一个profile方法,假装返回当前登录用户的数据:

// app/controller/User.php
namespace appcontroller;

class User
{
    public function profile()
    {
        // 这里假设我们已经通过了权限中间件,能拿到用户id
        return json([
            'code' => 0,
            'data' => [
                'id'   => request()->uid,
                'name' => '小明',
                'role' => 'editor'
            ]
        ]);
    }
}

注意,我们用request()->uid来获取当前请求的用户 ID,这个值我们会在权限中间件里塞进去。现在路由还没配,请求也跑不通,一步一步来。

二、第一个中间件:用 Token 验证用户身份

我们先把最常见的身份验证抽成一个中间件。在 ThinkPHP 8 中,中间件是一个普通的类,只需要实现handle方法,接收请求对象和一个闭包$next。当验证通过时,调用$next($request)把请求继续往下传;不通过时直接返回错误响应即可。

创建中间件文件

用命令行快速生成一个中间件骨架:

php think make:middleware Auth

命令会在app/middleware/Auth.php生成如下结构,我们往里填业务逻辑。

// app/middleware/Auth.php
namespace appmiddleware;

use thinkRequest;

class Auth
{
    public function handle(Request $request, Closure $next)
    {
        // 从请求头中获取 token,约定 Header 名为 Authorization
        $token = $request->header('Authorization');
        if (!$token) {
            return json([
                'code' => 401,
                'msg'  => '缺少认证令牌'
            ])->header(['Content-Type' => 'application/json']);
        }

        // 模拟通过 token 查询用户信息(实际项目中可能是解密JWT或查Redis)
        $user = $this->verifyToken($token);
        if (!$user) {
            return json([
                'code' => 401,
                'msg'  => '令牌无效或已过期'
            ])->header(['Content-Type' => 'application/json']);
        }

        // 把用户信息挂到 request 对象上,方便后续控制器使用
        $request->uid   = $user['id'];
        $request->role  = $user['role'];

        // 验证通过,继续执行后续中间件和控制器
        return $next($request);
    }

    // 私有方法:模拟 token 验证逻辑
    private function verifyToken($token)
    {
        // 这里简单地认为所有以 "token_" 开头的都是有效令牌
        if (strpos($token, 'token_') === 0) {
            // 模拟返回用户数据
            return [
                'id'   => 1001,
                'role' => 'editor'
            ];
        }
        return null;
    }
}

这个中间件做的事情很直接:检查 Header 里有没有Authorization,没有就返回 401;有就验证,验证失败也返回 401;验证成功就把用户 ID 和角色写到 request 上,然后放行。实际项目里,verifyToken会调用 JWT 解析或去缓存/数据库取用户信息,逻辑一样。

注册并使用中间件

中间件写好了,得让它生效。ThinkPHP 8 支持多种注册粒度:全局、路由分组、控制器、甚至方法级别。我们先用最简单的路由注册方式,让/user/profile接口走这个 Auth 中间件。

打开route/app.php,添加一条路由并指派中间件:

// route/app.php
use thinkfacadeRoute;

Route::get('user/profile', 'User/profile')->middleware(appmiddlewareAuth::class);

现在启动内置服务器(php think run),用 Postman 或 curl 测试一下:

curl -X GET http://localhost:8000/user/profile

会得到:{"code":401,"msg":"缺少认证令牌"}

带一个有效令牌:

curl -X GET http://localhost:8000/user/profile -H "Authorization: token_demo123"

返回:{"code":0,"data":{"id":1001,"name":"小明","role":"editor"}}

一个可用的权限中间件就这么上线了。后面任何需要身份验证的路由,只需在定义时链式调用->middleware(Auth::class)即可,控制器里不用写一句重复的判断。

三、第二个中间件:接口调用频率限制(Throttle)

权限问题解决了,接下来是另一个常见需求:限制某个用户或 IP 在固定时间内的请求次数。比如“同一个用户每分钟最多调用 10 次/user/profile”。我们来实现一个可配置的频率限制中间件。

创建 Throttle 中间件

php think make:middleware Throttle

打开app/middleware/Throttle.php,编写逻辑:

// app/middleware/Throttle.php
namespace appmiddleware;

use thinkfacadeCache;
use thinkRequest;

class Throttle
{
    // 默认限制:每分钟最多请求 10 次
    protected $maxAttempts    = 10;
    protected $decayMinutes   = 1;

    public function handle(Request $request, Closure $next, int $maxAttempts = null, int $decayMinutes = null)
    {
        // 允许通过路由参数覆盖默认值
        $maxAttempts  = $maxAttempts ?? $this->maxAttempts;
        $decayMinutes = $decayMinutes ?? $this->decayMinutes;

        // 生成唯一的缓存键,这里按用户 ID 区分,未登录用户按 IP 区分
        $userId = $request->uid ?? 0;
        if ($userId) {
            $key = 'throttle:user_' . $userId . '_' . $request->baseUrl();
        } else {
            $key = 'throttle:ip_' . $request->ip() . '_' . $request->baseUrl();
        }

        // 从缓存中获取当前请求次数
        $currentAttempts = Cache::get($key, 0);
        if ($currentAttempts >= $maxAttempts) {
            return json([
                'code' => 429,
                'msg'  => '请求过于频繁,请稍后再试'
            ])->header([
                'Content-Type'      => 'application/json',
                'Retry-After'       => $decayMinutes * 60
            ]);
        }

        // 次数加 1,并设置过期时间
        Cache::set($key, $currentAttempts + 1, $decayMinutes * 60);

        // 放行请求
        return $next($request);
    }
}

这里用到了 ThinkPHP 的Cache门面。缓存键由用户标识、请求 URL 组合而成,这样不同的接口可以有不同的限制。中间件的第三个和第四个参数$maxAttempts$decayMinutes允许在路由中传递,实现每个接口独立限制,非常灵活。

给路由加上限流

回到route/app.php,在我们之前的路由上叠加中间件,同时传递参数。修改成这样:

Route::get('user/profile', 'User/profile')
    ->middleware(appmiddlewareAuth::class)
    ->middleware(appmiddlewareThrottle::class, ['maxAttempts' => 5, 'decayMinutes' => 1]);

现在这个接口会先经过 Auth 验证,验证通过了再进入 Throttle 检查频率。一分钟内同一个用户请求超过 5 次就会收到 429 错误。

测试也很简单:快速连续执行 6 次带相同 token 的请求,第 6 次会返回{"code":429,"msg":"请求过于频繁,请稍后再试"}

注意中间件参数的传递方式:在middleware方法的第二个参数中传入一个关联数组,数组的键会自动匹配到handle方法的参数名。如果只传一个参数,也可以直接写,比如 ->middleware(Throttle::class, [10, 2])对应($maxAttempts, $decayMinutes)

四、中间件的执行顺序与分组复用

现在我们的路由上挂了两个中间件,一个 Auth,一个 Throttle。默认情况下,中间件的执行顺序就是它们注册的顺序:先 Auth,后 Throttle。这个顺序不能乱——如果 Throttle 跑到 Auth 前面,限流对象就只能是 IP,无法区分登录用户;而且 Auth 的request->uid还没赋值,Throttle 也只能走 IP 分支。

实际项目中,你可能会有一堆接口需要相同的中间件组合。比如所有/api/v1/*下的用户接口都需要 Auth + Throttle,而管理后台接口可能还需要额外一个“管理员角色验证”中间件。这时候路由分组就派上用场了。

Route::group('api/v1', function () {
    Route::get('user/profile', 'User/profile');
    Route::get('user/orders', 'User/orders');
})->middleware([
    appmiddlewareAuth::class,
    appmiddlewareThrottle::class . ':10,1', // 也支持这样的快捷传参
]);

如果某些接口需要覆盖默认限流参数,可以单独再链式调用一次middleware,后面的同类型中间件会覆盖前面分组里的配置吗?实际上 ThinkPHP 8 是追加执行的,如果同一个中间件类被添加两次,那么它也会执行两次。因此更推荐利用参数传递,或者在中间件内部通过$request属性来动态调整,而不是重复注册。

一个更干净的实践是:在中间件里写死默认值,路由分组中只指定需要特殊限制的接口,用参数覆盖。这样大部分接口走默认限流,少数敏感接口(如短信发送、支付)可以收紧限制。

五、再加一个“管理员专属”中间件

为了进一步展示中间件的组合能力,我们写一个简单的AdminRole中间件,判断当前用户角色是否为admin,不是就拒绝访问。然后把它挂到管理员专属的路由上。

php think make:middleware AdminRole
// app/middleware/AdminRole.php
namespace appmiddleware;

use thinkRequest;

class AdminRole
{
    public function handle(Request $request, Closure $next)
    {
        if (!isset($request->role) || $request->role !== 'admin') {
            return json([
                'code' => 403,
                'msg'  => '没有管理员权限'
            ]);
        }
        return $next($request);
    }
}

在路由分组中,我们可以为管理员路由单独加上这个中间件:

Route::group('admin', function () {
    Route::get('dashboard', 'Admin/dashboard');
})->middleware([
    appmiddlewareAuth::class,
    appmiddlewareAdminRole::class,
    appmiddlewareThrottle::class . ':20,1' // 管理员接口宽松一点
]);

这样一来,普通带 token 的用户访问/admin/dashboard会收到 403,只有令牌对应的角色是admin的用户才能通过。中间件的职责划分得非常清晰,每个文件只做一件事,控制器里完全不需要写权限判断代码。

六、中间件中处理异常与日志记录

到目前为止,我们的中间件返回的都是 JSON 响应,这是在 API 场景下的标准做法。如果你想在中间件里记录访问日志,可以直接在handle方法中注入日志门面。比如在 Throttle 中间件里记录被限流的请求:

use thinkfacadeLog;

// 在429返回之前
Log::warning('请求频率限制触发', [
    'ip'    => $request->ip(),
    'url'   => $request->url(),
    'uid'   => $userId
]);

如果你希望整个应用中的某些中间件在抛出异常时有统一的处理,可以写一个自定义的异常处理中间件,放在全局中间件的最外层,用try-catch包裹$next($request),但是 ThinkPHP 8 本身已有完善的异常处理机制,简单的需求用不到这么复杂的结构。

另外要注意,中间件里如果调用了halt()或直接exit,后续中间件和控制器都不会执行。所以返回响应时最好使用框架提供的json()辅助函数或者门面,保证数据格式一致。

七、总结与更多可能性

从最初的“控制器里到处是判断”到现在的“路由上声明式地挂载中间件”,权限验证和接口限流这两件事彻底从业务代码里分离了出来。你可以在新项目里沿用这个模式,也可以在老项目里逐步把重复逻辑提取为中间件,一点一点提高代码的清爽程度。

沿着今天这套思路,你还能扩展出很多实用的中间件:

  • 跨域处理中间件:统一添加 CORS 头,不必在每个控制器响应前手动写。
  • 请求签名校验:适用于开放平台 API,校验参数签名、时间戳抗重放。
  • 操作日志记录:自动记录每个修改类接口的请求参数、操作用户和响应状态。
  • 网站维护模式:全局中间件检查某个开关,维护期间所有接口直接返回维护提示。

ThinkPHP 8 的中间件机制还支持闭包定义、前置后置操作区分(在$next前后写逻辑),可以应对更复杂的场景。但当你遇到大部分需求时,像今天这样通过类文件配合参数传递就完全够用了。把框架提供的能力用在实际业务上,解决实际的代码复用问题,这才是技术实践的价值所在。

ThinkPHP 8 中间件深度实战:构建权限验证与接口频率限制的全面方案
收藏 (0) 打赏

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

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

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

淘吗网 thinkphp ThinkPHP 8 中间件深度实战:构建权限验证与接口频率限制的全面方案 https://www.taomawang.com/server/thinkphp/2287.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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