ThinkPHP项目IP黑名单与白名单:从基础配置到高级防御的完整实战指南
目录导读
- 为什么你的ThinkPHP项目需要IP访问控制?
- 基础篇:ThinkPHP6/8框架中的IP黑/白名单配置方法
- 1 使用中间件实现全局过滤
- 2 基于路由组的局部IP限制
- 3 配置文件动态读取IP列表
- 进阶篇:结合数据库与缓存的动态IP管理
- 1 设计IP规则数据表
- 2 使用Cache标签实现秒级生效
- 高级篇:异常检测与自动封禁策略
- 1 基于登录失败次数的自动拉黑
- 2 联合防火墙(如宝塔)的IPset联动
- 常见问题问答(FAQ)
- 性能优化与安全注意事项
为什么你的ThinkPHP项目需要IP访问控制?
在Web应用安全体系中,IP黑白名单是最基础但最有效的访问控制手段,对于ThinkPHP项目而言,无论是防止恶意爬虫抓取接口、抵御CC攻击,还是限制后台管理地址的访问来源,黑白名单都能提供第一道防线,根据OWASP Top 10的指导,未受控的访问请求是导致数据泄露的重要原因。

核心痛点:很多开发者直接使用Nginx或Apache做IP限制,但这种方式无法区分用户角色,也无法实现业务级别的动态封禁(某IP登录失败5次后自动拉黑10分钟),在ThinkPHP应用层实现IP过滤,能让你拥有业务逻辑的完全控制权。
基础篇:ThinkPHP6/8框架中的IP黑/白名单配置方法
1 使用中间件实现全局过滤
在ThinkPHP6/8中,推荐使用中间件来处理全局IP过滤,首先创建中间件类 app\middleware\IpGuard:
<?php
declare(strict_types=1);
namespace app\middleware;
use think\facade\Cache;
use think\Request;
use think\Response;
class IpGuard
{
public function handle(Request $request, \Closure $next)
{
$ip = $request->ip();
// 读取配置文件中的黑/白名单
$blackList = config('ipguard.black_list');
$whiteList = config('ipguard.white_list');
// 白名单优先:如果在白名单内,直接放行
if (in_array($ip, $whiteList)) {
return $next($request);
}
// 在黑名单内,直接拒绝
if (in_array($ip, $blackList)) {
return Response::create('Your IP is blocked', 'html', 403);
}
return $next($request);
}
}
然后在 app/middleware.php 中注册全局中间件:
return [
\app\middleware\IpGuard::class
];
2 基于路由组的局部IP限制
如果只想保护后台或特定API接口,可以在路由定义时追加中间件:
Route::group('admin', function () {
Route::get('dashboard', 'Admin/dashboard');
})->middleware(\app\middleware\AdminIpGuard::class);
这样,管理员可以针对后台设置更严格的白名单,而前台页面不受影响。
3 配置文件动态读取IP列表
在 config/ipguard.php 中定义静态列表:
return [
// 白名单IP,支持通配符 * 和网段掩码
'white_list' => [
'127.0.0.1',
'192.168.1.*',
'10.0.0.0/8'
],
'black_list' => [
'45.155.205.*',
'103.112.45.21'
]
];
注意:通用匹配需自己实现函数,但可以借助 think\facade\Validate 的 ip 规则,或者使用 ip2long 进行网段计算。
进阶篇:结合数据库与缓存的动态IP管理
静态配置无法满足运营需求,最佳实践是将IP规则存储于数据库,并通过 Redis或文件缓存 减少数据库查询压力。
1 设计IP规则数据表(tp_ip_rule)
CREATE TABLE `tp_ip_rule` ( `id` int(11) NOT NULL AUTO_INCREMENT, `ip` varchar(64) NOT NULL COMMENT '支持通配符/网段', `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1=黑名单 2=白名单', `expire_time` int(11) DEFAULT '0' COMMENT '过期时间戳,0为永久', `remark` varchar(255) DEFAULT '', PRIMARY KEY (`id`), KEY `ip_type` (`ip`,`type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2 使用Cache标签实现秒级生效
修改中间件逻辑,优先从缓存读取:
use think\facade\Db;
use think\facade\Cache;
public function handle(Request $request, \Closure $next)
{
$ip = $request->ip();
// 缓存Key,建议带上日期做每日轮换
$cacheKey = 'ip_rule_' . date('Ymd');
$rules = Cache::get($cacheKey);
if (!$rules) {
// 从数据库读取所有未过期的规则
$rules = Db::name('ip_rule')
->where('expire_time', '>', time())
->select()
->toArray();
Cache::set($cacheKey, $rules, 3600); // 缓存1小时
}
// 先判断白名单(如果白名单命中,直接允许)
foreach ($rules as $rule) {
if ($rule['type'] == 2 && ipMatch($ip, $rule['ip'])) {
return $next($request);
}
}
// 再判断黑名单
foreach ($rules as $rule) {
if ($rule['type'] == 1 && ipMatch($ip, $rule['ip'])) {
return Response::create('Access Denied', 'html', 403);
}
}
return $next($request);
}
写一个辅助函数 ipMatch() 来处理通配符/掩码逻辑。
高级篇:异常检测与自动封禁策略
1 基于登录失败次数的自动拉黑
在登录控制器中,当失败次数超限时,向 tp_ip_rule 写入临时黑名单:
use think\facade\Db;
use think\facade\Request;
$ip = Request::ip();
$failCount = Cache::get('login_fail_' . $ip, 0);
$failCount++;
if ($failCount >= 5) {
// 自动封禁30分钟
Db::name('ip_rule')->insert([
'ip' => $ip,
'type' => 1,
'expire_time' => time() + 1800,
'remark' => '登录失败自动封禁'
]);
Cache::delete('ip_rule_' . date('Ymd')); // 清缓存,立即生效
Cache::delete('login_fail_' . $ip);
} else {
Cache::set('login_fail_' . $ip, $failCount, 600); // 10分钟计数有效期
}
2 联合防火墙(如宝塔)的IPset联动
对于高并发攻击,应用层封禁已不足以应对,可以通过 shell_exec 调用系统防火墙命令:
shell_exec("iptables -A INPUT -s {$ip} -j DROP");
但务必注意安全,避免注入攻击。
常见问题问答(FAQ)
Q1:设置了白名单,为什么本机访问一直403?
答:请检查你的
white_list配置中是否包含了0.0.1或:1(IPv6),确认Request->ip()获取到的是代理IP,需要获取真实IP,建议在入口文件中设置$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'];。
Q2:如何快速测试IP匹配是否准确?
答:在控制器中临时
dump($request->ip());打印当前IP,然后对比规则,对于网段匹配,可以使用ip2long函数自己实现算法,或者引入symfony/http-foundation的IpUtils::checkIp()。
Q3:中间件和路由中间件的执行顺序是什么?
答:全局中间件在 应用初始化之后、路由匹配之前 执行,如果同时定义了路由组中间件,全局中间件先运行,若全局放行则进入路由中间件,如果全局拦截,则路由中间件不执行。
Q4:缓存的IP规则过期后如何动态更新?
答:使用上述方案,当新增规则时,主动删除
ip_rule_前缀的缓存Key,或者在后台管理操作封装一个clearIpRuleCache()方法。
Q5:如果IP为IPv6地址,如何处理?
答:建议将数据库字段长度扩展到64字节,并使用
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)检测,通配符规则对于IPv6不友好,建议只支持具体IP或网段格式。
性能优化与安全注意事项
- 不要将黑名单放在最前:白名单命中率通常较低,黑名单较高,建议先查缓存,再查库。
- 数据库查询必须走索引:
ip字段需建立索引,否则高流量下数据库压力较大。 - 缓存使用 Redis:文件缓存并发下可能会有锁竞争,生产环境建议用 Redis。
- 日志记录:每次拦截都要记录日志,便于后续分析攻击来源。
- 绕过风险:攻击者可伪造
X-Forwarded-For头,务必使用可靠的真实IP获取方案(如Nginx设置set_real_ip_from)。
最后建议:IP黑白名单只是纵深防御的一部分,还应结合验证码、频率限制、WAF等,希望本指南能帮助你构建一个健壮的ThinkPHP访问控制系统,如果有更多细节问题,欢迎在评论区留言讨论。