功能需求
- 支援單一 IP:
192.168.1.100
- 支援 CIDR 區塊:
192.168.1.0/24
- 支援範圍格式:
192.168.1.[10-20]
- 支援萬用字元:
192.168.*.*
- 設定檔驅動,一行一條規則
核心實作
<?php
/**
* 檢查 IP 是否符合規則
* @param string $rule 規則字串
* @param string $ip 待檢查 IP(預設自動取得 REMOTE_ADDR)
* @return bool
*/
function matchIpRule(string $rule, string $ip = ''): bool {
if ($ip === '') $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
if ($ip === '127.0.0.1') return true; // 本地永遠允許
$rule = trim($rule);
if ($rule === '') return false;
// CIDR 格式:192.168.1.0/24
if (preg_match('/^(\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})$/', $rule, $m)) {
$rangeIp = ip2long($m[1]);
$mask = $m[2];
$ipLong = ip2long($ip);
$netmask = -1 << (32 - $mask);
return ($ipLong & $netmask) === ($rangeIp & $netmask);
}
// 範圍格式:192.168.1.[10-20] 或 192.168.1.10-192.168.1.20
if (preg_match('/^(\d{1,3}(?:\.\d{1,3}){3})\[(\d+)-(\d+)\]$/', $rule, $m)) {
$base = $m[1];
$start = (int)$m[2];
$end = (int)$m[3];
$ipParts = explode('.', $ip);
$baseParts = explode('.', $base);
// 前三段必須相同
if ($ipParts[0] !== $baseParts[0] || $ipParts[1] !== $baseParts[1] || $ipParts[2] !== $baseParts[2]) {
return false;
}
$lastOctet = (int)$ipParts[3];
return $lastOctet >= $start && $lastOctet <= $end;
}
// 萬用字元:192.168.*.*
if (strpos($rule, '*') !== false) {
$pattern = '^' . str_replace(['.', '*'], ['\.', '\d{1,3}'], preg_quote($rule, '/')) . '$';
return (bool)preg_match('/' . $pattern . '/', $ip);
}
// 精確比對
return $rule === $ip;
}
/**
* 從設定檔載入規則並檢查
* @param string $configPath 設定檔路徑
* @param string $ip 待檢查 IP
* @return bool true=允許, false=封鎖
*/
function checkIpAccess(string $configPath, string $ip = ''): bool {
if (!is_file($configPath)) return true; // 無設定檔預設允許
$handle = fopen($configPath, 'r');
if (!$handle) return true;
while (($line = fgets($handle)) !== false) {
$line = trim($line);
// 跳過空行與註解
if ($line === '' || $line[0] === '#') continue;
// 移除行內註解
if (($pos = strpos($line, '#')) !== false) $line = trim(substr($line, 0, $pos));
if (matchIpRule($line, $ip)) {
fclose($handle);
return true; // 符合允許規則
}
}
fclose($handle);
return false; // 無符合規則,預設封鎖
}
設定檔範例 ip.allow
# 允許名單(白名單模式)
127.0.0.1
192.168.1.100 # 單一 IP
10.0.0.0/8 # 整個 A 類網段
172.16.0.0/12 # 私有 B 類網段
192.168.0.0/16 # 私有 C 類網段
192.168.1.[10-50] # 範圍
203.45.*.* # 萬用字元
整合至應用程式
<?php
// bootstrap.php 或 index.php 最頂端
require 'ip_filter.php';
$allowed = checkIpAccess(__DIR__ . '/config/ip.allow');
if (!$allowed) {
http_response_code(403);
die('您的 IP 已被封鎖,如有疑問請聯繫管理員。');
}
// 繼續正常執行...
黑名單模式變體
function checkIpBlocked(string $blocklistPath, string $ip = ''): bool {
// 邏輯相反:符合規則即封鎖
// 設定檔命名 ip.deny
// 回傳 true 表示被封鎖
}
效能優化建議
- 將 CIDR 規則預先轉換為
['start' => long, 'end' => long] 陣列,避免每次請求重複計算。
- 使用 APCu 或 Redis 快取編譯後的規則陣列。
- 高流量系統建議移至 Nginx
ngx_http_access_module 或防火牆層(iptables/ipset)處理。