在Web应用开发中,中间件(Middleware)是处理HTTP请求和响应的核心组件。ThinkPHP 8 提供了强大且灵活的中间件系统,允许开发者在请求到达控制器之前或响应返回客户端之前插入自定义逻辑。通过中间件,我们可以优雅地实现身份验证、请求日志、跨域处理、数据加密、接口限流等通用功能,而无需在每个控制器方法中重复编写代码。本文将通过三个完整的实战案例,带你系统掌握ThinkPHP 8中间件的开发与应用。
理解ThinkPHP 8中间件机制
在ThinkPHP 8中,中间件是请求生命周期中的拦截器。当一个HTTP请求到达时,它会先经过一系列中间件的处理,最后才到达控制器;控制器返回响应后,中间件还可以对响应进行加工,然后返回给客户端。这种“洋葱模型”使得通用逻辑得以集中管理。
中间件的典型应用场景包括:
- 身份认证:验证请求中的Token或Session,拒绝未授权访问。
- 日志记录:自动记录每个请求的URL、参数、执行时间和响应状态。
- 跨域处理:统一添加CORS响应头,支持前后端分离开发。
- 数据过滤:对输入参数进行XSS过滤或SQL注入防护。
- 接口限流:基于IP或用户ID限制单位时间内的请求次数。
与以往在基类控制器中编写initialize方法相比,中间件具有更细粒度的控制能力——你可以将某个中间件只应用于特定路由,也可以按分组批量应用,灵活性与可维护性都大大提升。
中间件的定义与注册方式
ThinkPHP 8的中间件类通常放在app/middleware目录下。一个中间件类需要包含一个handle方法,该方法接收两个参数:$request(请求对象)和$next(回调闭包)。在handle方法中,你可以在调用$next($request)之前执行前置逻辑,在调用之后执行后置逻辑。
最基本的中间件结构如下:
// app/middleware/CheckToken.php
namespace appmiddleware;
class CheckToken
{
public function handle($request, Closure $next)
{
// 前置逻辑:在请求到达控制器之前执行
$token = $request->header('Authorization');
if (empty($token)) {
return json(['code' => 401, 'msg' => '缺少认证Token'])->code(401);
}
// 验证Token逻辑...
// 通过$next将请求传递给下一个中间件或控制器
$response = $next($request);
// 后置逻辑:在响应返回客户端之前执行(可选)
// $response->header('X-Powered-By', 'ThinkPHP');
return $response;
}
}
定义好中间件后,需要在app/middleware.php中注册(用于全局中间件),或者在路由配置中指定(用于路由级中间件)。
// app/middleware.php — 全局中间件注册
return [
// 全局中间件,按数组顺序执行
appmiddlewareCors::class,
appmiddlewareRequestLog::class,
];
路由级中间件注册将在后续案例中详细展示。
案例一:API Token鉴权中间件
在前后端分离的项目中,API通常使用Token(如JWT)进行身份验证。编写一个通用的Token鉴权中间件,可以避免在每个控制器方法中重复验证逻辑。
首先,创建中间件类app/middleware/ApiAuth.php:
// app/middleware/ApiAuth.php
namespace appmiddleware;
use thinkfacadeCache;
use appmodelUser;
class ApiAuth
{
public function handle($request, Closure $next)
{
// 从请求头获取Token
$token = $request->header('Authorization');
// 移除可能存在的 "Bearer " 前缀
if (str_starts_with($token, 'Bearer ')) {
$token = substr($token, 7);
}
if (empty($token)) {
return json([
'code' => 401,
'msg' => '请先登录再访问'
])->code(401);
}
// 从缓存中根据Token获取用户ID(假设登录时将Token存入Redis)
$userId = Cache::get('token:' . $token);
if (!$userId) {
return json([
'code' => 401,
'msg' => 'Token已过期或无效,请重新登录'
])->code(401);
}
// 查询用户信息并注入到请求对象中,供后续控制器使用
$user = User::field('id,nickname,avatar,status')->find($userId);
if (!$user || $user->status !== 1) {
return json([
'code' => 403,
'msg' => '账号已被禁用或不存在'
])->code(403);
}
// 将用户信息附加到请求对象
$request->userInfo = $user;
// 续期Token缓存(延长有效期)
Cache::set('token:' . $token, $userId, 7200);
// 继续执行后续中间件和控制器
return $next($request);
}
}
接下来,在路由文件中将该中间件应用到需要鉴权的路由组:
// route/app.php
use thinkfacadeRoute;
// 不需要鉴权的公开路由
Route::post('api/login', 'api/Login/login');
Route::post('api/register', 'api/Register/register');
// 需要鉴权的路由组
Route::group('api', function () {
Route::get('user/profile', 'api/User/profile');
Route::put('user/profile', 'api/User/updateProfile');
Route::get('orders', 'api/Order/index');
Route::post('orders', 'api/Order/create');
})->middleware(appmiddlewareApiAuth::class);
在控制器中,可以直接通过$request->userInfo获取当前登录用户的信息,无需再次查询数据库:
// app/controller/api/User.php
namespace appcontrollerapi;
class User
{
public function profile($request)
{
$user = $request->userInfo;
return json([
'code' => 200,
'data' => [
'id' => $user->id,
'nickname' => $user->nickname,
'avatar' => $user->avatar,
]
]);
}
}
这个中间件带来的收益非常明显:所有受保护的路由都会被自动拦截验证,控制器只需关注业务逻辑,代码简洁且安全可靠。
案例二:操作日志记录中间件
在实际项目中,我们常常需要记录每个API请求的详细信息——包括请求路径、参数、执行耗时、响应状态码等,以便排查问题和审计。通过日志中间件,可以零侵入地实现这一功能。
创建app/middleware/RequestLog.php:
// app/middleware/RequestLog.php
namespace appmiddleware;
use thinkfacadeLog;
class RequestLog
{
public function handle($request, Closure $next)
{
// 记录请求开始时间(毫秒级)
$startTime = microtime(true);
// 组装请求基础信息
$logData = [
'method' => $request->method(),
'url' => $request->url(true),
'ip' => $request->ip(),
'user_agent' => $request->header('User-Agent', ''),
'params' => $request->param(),
'timestamp' => date('Y-m-d H:i:s'),
];
// 记录请求日志
Log::info('[请求] ' . json_encode($logData, JSON_UNESCAPED_UNICODE));
try {
// 继续执行后续中间件和控制器
$response = $next($request);
// 计算执行耗时
$duration = round((microtime(true) - $startTime) * 1000, 2);
// 获取响应状态码
$httpCode = $response->getCode();
// 记录响应日志
$responseLog = [
'url' => $logData['url'],
'status' => $httpCode,
'duration' => $duration . 'ms',
];
if ($httpCode >= 400) {
Log::error('[响应异常] ' . json_encode($responseLog, JSON_UNESCAPED_UNICODE));
} else {
Log::info('[响应] ' . json_encode($responseLog, JSON_UNESCAPED_UNICODE));
}
return $response;
} catch (Throwable $e) {
// 捕获异常,记录错误日志后重新抛出
$duration = round((microtime(true) - $startTime) * 1000, 2);
Log::error('[请求异常] URL: ' . $logData['url'] .
', 耗时: ' . $duration . 'ms, ' .
'错误: ' . $e->getMessage());
throw $e;
}
}
}
这个中间件建议注册为全局中间件(在app/middleware.php中),因为日志记录通常是全局需求:
// app/middleware.php
return [
appmiddlewareRequestLog::class,
// 其他全局中间件...
];
执行后的日志文件(runtime/log目录下)将包含类似以下的记录:
[2024-12-19T10:23:45+08:00] [INFO] [请求] {"method":"GET","url":"http://api.example.com/api/user/profile?id=1","ip":"192.168.1.100","user_agent":"Mozilla/5.0...","params":{"id":"1"},"timestamp":"2024-12-19 10:23:45"}
[2024-12-19T10:23:45+08:00] [INFO] [响应] {"url":"http://api.example.com/api/user/profile?id=1","status":200,"duration":"12.35ms"}
通过这种方式,你可以在不侵入业务代码的情况下完整追踪每个请求的来龙去脉,极大方便了问题排查和性能监控。
案例三:CORS跨域处理中间件
前后端分离架构中,浏览器会对跨域请求进行拦截。虽然Nginx等反向代理可以处理跨域,但在开发阶段或某些部署场景下,通过在应用中添加CORS中间件是更灵活的做法。
创建app/middleware/Cors.php:
// app/middleware/Cors.php
namespace appmiddleware;
class Cors
{
public function handle($request, Closure $next)
{
// 允许的域名列表(生产环境应限制为具体域名)
$allowOrigins = [
'http://localhost:3000',
'http://localhost:8080',
'https://admin.yourapp.com',
];
$origin = $request->header('Origin', '');
// 设置通用的CORS响应头
$headers = [
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
'Access-Control-Max-Age' => 86400,
'Access-Control-Allow-Credentials' => 'true',
];
// 如果请求来源在允许列表中,则设置对应的Origin
if (in_array($origin, $allowOrigins)) {
$headers['Access-Control-Allow-Origin'] = $origin;
} elseif (!empty($allowOrigins)) {
// 如果来源不在列表中,默认使用第一个允许的域名
$headers['Access-Control-Allow-Origin'] = $allowOrigins[0];
}
// 处理OPTIONS预检请求
if ($request->method() === 'OPTIONS') {
return response('', 204)->header($headers);
}
// 正常请求继续处理
$response = $next($request);
// 将CORS头添加到响应中
foreach ($headers as $key => $value) {
$response->header($key, $value);
}
return $response;
}
}
CORS中间件通常也应该注册为全局中间件,因为它需要处理所有可能的API请求:
// app/middleware.php
return [
appmiddlewareCors::class,
appmiddlewareRequestLog::class,
];
注意中间件在数组中的顺序非常重要——CORS中间件放在第一位,确保预检请求(OPTIONS)能在最外层被快速响应,避免进入后续的认证或日志逻辑。
中间件分组与执行顺序管理
随着项目的增长,中间件数量可能越来越多。ThinkPHP 8支持中间件分组,允许你将多个中间件打包成一个别名,在路由中方便地引用。
在app/middleware.php中定义分组:
// app/middleware.php
return [
// 全局中间件
appmiddlewareCors::class,
appmiddlewareRequestLog::class,
// 定义中间件别名(非全局,需在路由中显式引用)
'auth' => appmiddlewareApiAuth::class,
'admin' => appmiddlewareAdminAuth::class,
'throttle' => appmiddlewareRateLimit::class,
// 定义中间件组:将多个中间件捆绑
'api_group' => [
appmiddlewareApiAuth::class,
appmiddlewareRateLimit::class,
],
];
在路由中使用分组别名:
// route/app.php
use thinkfacadeRoute;
// 使用单个中间件别名
Route::get('api/public', 'Index/index')->middleware('auth');
// 使用中间件组
Route::group('api/v2', function () {
Route::get('profile', 'api/User/profile');
Route::get('orders', 'api/Order/index');
})->middleware('api_group');
关于执行顺序,ThinkPHP 8遵循以下规则:
- 全局中间件按
middleware.php中的数组顺序依次执行。 - 路由中间件在全局中间件之后执行,同样按路由定义的顺序。
- 每个中间件中,
$next($request)之前的代码是前置处理,之后的代码是后置处理。前置按注册顺序执行,后置按注册顺序反向执行(真正的洋葱模型)。
例如,全局中间件A、B,路由中间件C、D,则前置执行顺序为 A→B→C→D→控制器,后置执行顺序为 控制器→D→C→B→A。
全局中间件与路由中间件对比
选择全局中间件还是路由中间件,取决于功能的覆盖范围:
| 维度 | 全局中间件 | 路由中间件 |
|---|---|---|
| 注册位置 | app/middleware.php |
路由定义中(route/app.php) |
| 作用范围 | 所有请求 | 指定的路由或路由组 |
| 典型用途 | CORS、全局日志、安全过滤 | 身份鉴权、角色权限、接口限流 |
| 灵活性 | 较低,无法对特定路由排除 | 高,可按路由精细控制 |
| 性能影响 | 每个请求都执行,需保证轻量 | 仅匹配路由时执行 |
在实践中,建议将轻量且通用的功能(如CORS、请求日志)设为全局中间件,而将与业务逻辑相关的功能(如Token验证、权限检查)设为路由中间件,以获得最佳的灵活性和性能。
最佳实践与注意事项
在实际项目中使用中间件时,以下几条经验准则可以帮助你避免常见陷阱:
1. 保持中间件单一职责
每个中间件应该只做一件事。例如,鉴权中间件不应该同时处理日志记录;跨域中间件不应该包含Token验证逻辑。单一职责让中间件更容易测试、复用和维护。当功能变得复杂时,拆分为多个独立的中间件,利用分组组合使用。
2. 正确处理中间件中的异常
中间件中抛出的异常如果没有被捕获,会中断整个请求链。对于可恢复的错误(如Token过期),建议在中间件中直接返回JSON响应,而不是抛出异常。对于不可恢复的错误(如数据库连接失败),可以抛出异常,由全局异常处理器统一返回友好的错误信息。
3. 利用请求对象传递中间件数据
中间件处理过程中产生的数据(如案例一中的$request->userInfo),可以通过请求对象传递给后续的中间件和控制器。这是一种轻量级的数据共享方式,避免了使用全局变量或静态属性。
4. 注意中间件的执行开销
全局中间件会在每个请求中执行,因此务必保持其代码轻量。避免在全局中间件中进行远程调用、大量数据库查询或复杂计算。如果某个中间件的初始化成本较高,考虑将其改为路由中间件,仅应用于必要的路由。
5. 编写中间件单元测试
中间件是独立的可测试单元。在测试中,你可以创建一个模拟的$request对象和一个返回固定响应的$next闭包,从而验证中间件的行为是否符合预期。这比在完整应用中进行集成测试更加高效。
6. 使用中间件参数实现动态配置
ThinkPHP 8支持向中间件传递参数,实现更灵活的行为控制。例如,为限流中间件传入不同的频率限制:
// 路由中传递参数
Route::get('api/search', 'api/Search/index')
->middleware('throttle:60,1'); // 每分钟60次
Route::post('api/login', 'api/Login/login')
->middleware('throttle:5,1'); // 每分钟5次
在中间件中接收参数:
// app/middleware/RateLimit.php
public function handle($request, Closure $next, int $maxRequests = 60, int $windowMinutes = 1)
{
// $maxRequests = 60, $windowMinutes = 1
// 实现限流逻辑...
return $next($request);
}
总结
中间件是ThinkPHP 8框架中最为强大的特性之一。通过本文的三个实战案例——API Token鉴权、操作日志记录和CORS跨域处理,我们全面覆盖了中间件的定义、注册、分组和参数传递等核心知识点。掌握中间件开发后,你将能够以非侵入的方式为应用添加横切关注点,使控制器层专注于业务逻辑,代码结构更加清晰、可维护性大幅提升。
在实际项目中,建议从全局中间件(CORS、日志)开始实践,逐步扩展到业务相关的路由中间件(鉴权、限流),最终形成一套完善的中间件体系。配合ThinkPHP 8的事件系统和队列机制,你可以构建出高度解耦、易于扩展的现代化PHP应用。

