前置中间件的核心是将$next($request)置于校验之后、返回之前;host校验需拼接端口并兼容代理;referer高危接口用abort(403)保日志;ip黑名单必须缓存查且严格匹配;中间件注册顺序须在最前。

前置中间件的核心写法:$next() 必须放在最后
ThinkPHP 的前置逻辑不是靠“命名”或“注册位置”决定的,而是由 $next($request) 在 handle() 方法里的位置决定的。只要它出现在所有校验代码之后、方法返回之前,就是真正的前置拦截。
常见错误是把 $next($request) 写在开头,或者用 die()/exit() 强制终止——前者会让后续中间件和控制器照常执行,后者跳过框架响应生命周期,日志、Header、缓存等全部丢失。
- 正确结构:
if (非法条件) { return response(...)->code(403); }→ 后续校验 →return $next($request); - 禁止在前置逻辑里调用
$next($request)之前就返回响应体以外的内容(比如 echo、print_r) - 不要在
__construct()里读$request->session()或$request->user(),此时 request 尚未完成绑定
Host 头校验必须包含端口,否则本地开发直接 403
伪造 Host 是扫描器常用手法,但校验时漏掉端口会导致 localhost:8000 被当成非法域名。ThinkPHP 的 $request->host() 默认不带端口,需手动拼接。
- 兼容代理环境的写法:
$host = $request->header('host') ?: $request->host() . ($request->isSsl() ? ':443' : ':80'); - 白名单配置必须显式写全:
['admin.example.com', 'localhost:8000', '127.0.0.1:8000'] - 不能只用
str_contains($host, 'example.com'),否则evil.example.com也能过
Referer 校验该用 abort() 还是 response()?看是否要留日志
统一用 response() 简单粗暴,但会绕过框架异常处理流程;对敏感接口(如 /api/export)建议用 abort(403),它触发全局异常处理器,保留日志、监控上报、Trace ID 等关键上下文。
- 通用校验(如全站防盗链):
if (empty($referer) || !in_array($referer, config('referer.whitelist'))) { return response('', 403); } - 高风险路径精细化控制:
if ($request->path() === 'api/export' && !str_starts_with($referer, 'https://admin.example.com')) { abort(403); } - 注意:
abort()不会自动写日志,需确保app/exception/Handler.php中已启用 4xx 日志记录
IP 黑名单必须用 cache() 查,别查数据库
每次请求都查 DB 会拖垮性能,且黑名单更新频率低。用缓存查是硬性要求,但要注意缓存键名统一、过期时间合理、以及 $request->ip(true) 是否开启代理识别。
- 获取真实 IP 必须传
true:$ip = $request->ip(true);(否则 Nginx 反代后永远是 127.0.0.1) - 缓存键固定为
ip_blacklist,避免多处维护不同 key 导致漏判 - 写入黑名单示例:
cache('ip_blacklist', array_merge(cache('ip_blacklist', []), [$ip]), 3600); - 不要用
in_array($ip, $blacklist, true)的松散比较,IP 字符串必须严格匹配
config/middleware.php 的最前面,且不能被日志、语言包、Session 初始化等中间件隔开——顺序错一格,Host 或 Referer 就可能被后续中间件修改或覆盖。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











