ThinkPHP项目IP黑名单与白名单

wen PHP项目 3

ThinkPHP项目IP黑名单与白名单:从基础配置到高级防御的完整实战指南

目录导读

  1. 为什么你的ThinkPHP项目需要IP访问控制?
  2. 基础篇:ThinkPHP6/8框架中的IP黑/白名单配置方法
    • 1 使用中间件实现全局过滤
    • 2 基于路由组的局部IP限制
    • 3 配置文件动态读取IP列表
  3. 进阶篇:结合数据库与缓存的动态IP管理
    • 1 设计IP规则数据表
    • 2 使用Cache标签实现秒级生效
  4. 高级篇:异常检测与自动封禁策略
    • 1 基于登录失败次数的自动拉黑
    • 2 联合防火墙(如宝塔)的IPset联动
  5. 常见问题问答(FAQ)
  6. 性能优化与安全注意事项

为什么你的ThinkPHP项目需要IP访问控制?

在Web应用安全体系中,IP黑白名单是最基础但最有效的访问控制手段,对于ThinkPHP项目而言,无论是防止恶意爬虫抓取接口、抵御CC攻击,还是限制后台管理地址的访问来源,黑白名单都能提供第一道防线,根据OWASP Top 10的指导,未受控的访问请求是导致数据泄露的重要原因。

ThinkPHP项目IP黑名单与白名单

核心痛点:很多开发者直接使用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\Validateip 规则,或者使用 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-foundationIpUtils::checkIp()

Q3:中间件和路由中间件的执行顺序是什么?

答:全局中间件在 应用初始化之后路由匹配之前 执行,如果同时定义了路由组中间件,全局中间件先运行,若全局放行则进入路由中间件,如果全局拦截,则路由中间件不执行。

Q4:缓存的IP规则过期后如何动态更新?

答:使用上述方案,当新增规则时,主动删除 ip_rule_ 前缀的缓存Key,或者在后台管理操作封装一个 clearIpRuleCache() 方法。

Q5:如果IP为IPv6地址,如何处理?

答:建议将数据库字段长度扩展到64字节,并使用 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) 检测,通配符规则对于IPv6不友好,建议只支持具体IP或网段格式。

性能优化与安全注意事项

  1. 不要将黑名单放在最前:白名单命中率通常较低,黑名单较高,建议先查缓存,再查库。
  2. 数据库查询必须走索引ip 字段需建立索引,否则高流量下数据库压力较大。
  3. 缓存使用 Redis:文件缓存并发下可能会有锁竞争,生产环境建议用 Redis。
  4. 日志记录:每次拦截都要记录日志,便于后续分析攻击来源。
  5. 绕过风险:攻击者可伪造 X-Forwarded-For 头,务必使用可靠的真实IP获取方案(如Nginx设置 set_real_ip_from)。

最后建议:IP黑白名单只是纵深防御的一部分,还应结合验证码、频率限制、WAF等,希望本指南能帮助你构建一个健壮的ThinkPHP访问控制系统,如果有更多细节问题,欢迎在评论区留言讨论。

抱歉,评论功能暂时关闭!