專案常需封鎖惡意 IP 或特定區段。單純字串比對 (stripos) 存在誤判(如 1.2.3.4 會誤擋 11.2.3.4)且效能差。本文提供三層次解決方案:簡易版、CIDR 精確版、生產級 Redis 快取版,並分析效能與安全性。
// ban.txt 每行一個 IP
$banList = file('ban.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$banSet = array_flip($banList); // O(1) 查找
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (isset($banSet[$clientIp])) {
http_response_code(403);
exit("Access denied: Your IP ($clientIp) is banned.");
}
/**
* 檢查 IP 是否在任一 CIDR 區段內
* @param string $ip 客戶端 IP
* @param array $cidrs 如 ['192.168.1.0/24', '10.0.0.0/8']
* @return bool
*/
function ipInCidrs(string $ip, array $cidrs): bool
{
// IPv4 轉整數
$ipLong = ip2long($ip);
if ($ipLong === false) return false; // 無效 IP
foreach ($cidrs as $cidr) {
[$range, $bits] = explode('/', $cidr);
$mask = ~((1 << (32 - $bits)) - 1);
$net = ip2long($range) & $mask;
if (($ipLong & $mask) === $net) {
return true;
}
}
return false;
}
// 使用範例
$cidrs = [
'192.168.1.0/24', // 封鎖整個 192.168.1.x
'10.0.0.0/8', // 封鎖 10.x.x.x
'203.0.113.5/32', // 封鎖單一 IP (等同 /32)
];
if (ipInCidrs($_SERVER['REMOTE_ADDR'], $cidrs)) {
http_response_code(403);
exit('Access denied');
}
// Redis 儲存結構
// Key: ban:ips (Set) 成員: "192.168.1.100", "10.0.0.0/8"
// Key: ban:cidr_masks (Hash) 欄位: "8" -> "255.0.0.0", "24" -> "255.255.255.0" 預計算遮罩
// Lua 腳本:原子檢查 IP 是否命中(含 CIDR)
$luaScript = <<connect('127.0.0.1', 6379);
$sha = $redis->script('LOAD', $luaScript);
$clientIp = $_SERVER['REMOTE_ADDR'];
$banned = $redis->evalSha($sha, 2, 'ban:ips', 'ban:cidr_masks', $clientIp);
if ($banned) {
http_response_code(403);
exit('Access denied');
}
| 議題 | 風險 | 對策 |
|---|---|---|
| 代理/負載平衡器後取得真實 IP | REMOTE_ADDR 為代理 IP | 信任代理標頭:$_SERVER['HTTP_X_FORWARDED_FOR'](需驗證代理鏈) |
| IPv6 支援 | ip2long 僅支援 IPv4 | 使用 inet_pton() + 128-bit 整數或直接字串比對 CIDR |
| 封鎖清單動態更新 | 檔案讀取需重啟進程或重新 require | Redis 方案支援熱更新;檔案方案用 clearstatcache() + 定時重載 |
| 大量 CIDR 規則效能 | PHP 迴圈檢查慢 | Lua 腳本在 Redis 內原子執行,微秒級延遲 |
| 誤封合法使用者 | 無審核機制 | 加入白名單優先檢查、記錄封鎖日誌供稽核 |
deny、Apache Require not ip)優先攔截,減少 PHP 執行開銷。IP 封鎖看似簡單,但涉及網路拓撲、協定細節、效能權衡。選對層級、用對資料結構,才能在不犧牲效能的前提下精準防護。