API 保護、後台管理、內部系統常需限制存取來源 IP。本文提供生產級 IpAccessControl 類別,支援 IPv4/IPv6、CIDR 區段(192.168.1.0/24)、萬用字元(192.168.*.*)、多層代理穿透(X-Forwarded-For)、快取優化,並說明部署注意事項。
<?php
class IpAccessControl
{
private array $whitelist = []; // 允許清單(空=全允許)
private array $blacklist = []; // 封鎖清單(優先級高於白名單)
private array $compiledRules = []; // 編譯後的正則/網段物件
private bool $strictProxyCheck = true; // 嚴格驗證代理標頭
public function __construct(array $config = [])
{
$this->whitelist = $config['whitelist'] ?? [];
$this->blacklist = $config['blacklist'] ?? [];
$this->strictProxyCheck = $config['strict_proxy_check'] ?? true;
$this->compileRules();
}
/** 編譯規則為高效判斷單元 */
private function compileRules(): void
{
$this->compiledRules = [
'whitelist' => $this->parseRules($this->whitelist),
'blacklist' => $this->parseRules($this->blacklist),
];
}
/** 解析單條規則:支援 CIDR、萬用字元、單一 IP */
private function parseRules(array $rules): array
{
$compiled = [];
foreach ($rules as $rule) {
$rule = trim($rule);
if ($rule === '') continue;
// CIDR 格式:192.168.1.0/24 或 2001:db8::/32
if (str_contains($rule, '/')) {
$compiled[] = ['type' => 'cidr', 'value' => $rule];
continue;
}
// 萬用字元:192.168.*.* 或 10.*.*.*
if (str_contains($rule, '*')) {
$regex = '^' . str_replace(['*', '.'], ['\d+', '\.'], preg_quote($rule, '/')) . '$';
$compiled[] = ['type' => 'regex', 'value' => $regex];
continue;
}
// 純 IP
$compiled[] = ['type' => 'exact', 'value' => $rule];
}
return $compiled;
}
/** 核心判斷:取得客戶端真實 IP 並比對規則 */
public function checkAccess(): bool
{
$clientIp = $this->getClientIp();
// 1. 黑名單優先:命中即拒絕
if ($this->matchRules($clientIp, $this->compiledRules['blacklist'])) {
return false;
}
// 2. 白名單為空 = 不限制
if (empty($this->compiledRules['whitelist'])) {
return true;
}
// 3. 白名單:必須命中才允許
return $this->matchRules($clientIp, $this->compiledRules['whitelist']);
}
/** 比對單一 IP 對規則陣列 */
private function matchRules(string $ip, array $rules): bool
{
foreach ($rules as $rule) {
switch ($rule['type']) {
case 'exact':
if ($ip === $rule['value']) return true;
break;
case 'regex':
if (preg_match('/' . $rule['value'] . '/', $ip)) return true;
break;
case 'cidr':
if ($this->ipInCidr($ip, $rule['value'])) return true;
break;
}
}
return false;
}
/** CIDR 區段判斷(支援 IPv4/IPv6) */
private function ipInCidr(string $ip, string $cidr): bool
{
[$rangeIp, $bits] = explode('/', $cidr);
$bits = (int)$bits;
// 使用 PHP 內建函數轉換為二進位
$ipBin = inet_pton($ip);
$rangeBin = inet_pton($rangeIp);
if ($ipBin === false || $rangeBin === false) return false; // 格式錯誤
$family = strlen($ipBin) === 4 ? AF_INET : AF_INET6;
$maxBits = $family === AF_INET ? 32 : 128;
if ($bits < 0 || $bits > $maxBits) return false;
// 建立遮罩
$mask = $bits === 0 ? '' : str_repeat('f', $bits / 4);
if ($bits % 4 !== 0) {
$mask .= dechex(0xF0 >> ($bits % 4));
}
$mask = str_pad($mask, $maxBits / 4, '0', STR_PAD_RIGHT);
$maskBin = hex2bin($mask);
// 比較網路位址部分
return ($ipBin & $maskBin) === ($rangeBin & $maskBin);
}
/** 取得客戶端真實 IP(支援代理穿透) */
public function getClientIp(): string
{
// 1. 檢查 HTTP_CLIENT_IP(某些代理設定)
if (isset($_SERVER['HTTP_CLIENT_IP']) && $this->isValidIp($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
// 2. 檢查 X-Forwarded-For(標準代理鏈,取第一個非私有 IP)
elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
$ip = '0.0.0.0'; // fallback
foreach ($ips as $forwardedIp) {
if ($this->isValidIp($forwardedIp) && !$this->isPrivateIp($forwardedIp)) {
$ip = $forwardedIp;
break;
}
}
// 全是私有 IP,取第一個
if ($ip === '0.0.0.0' && $ips) $ip = $ips[0];
}
// 3. 直接連線
elseif (isset($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
} else {
$ip = '0.0.0.0';
}
return $this->normalizeIp($ip);
}
private function isValidIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
|| filter_var($ip, FILTER_VALIDATE_IP) !== false; // 允許私有/保留 IP 通過格式檢查
}
private function isPrivateIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false
&& filter_var($ip, FILTER_VALIDATE_IP) !== false;
}
private function normalizeIp(string $ip): string
{
// IPv6 回環地址正規化
if ($ip === '::1') return '127.0.0.1';
// IPv4 對應 IPv6 格式 ::ffff:192.168.1.1
if (str_starts_with($ip, '::ffff:')) return substr($ip, 7);
return $ip;
}
// ========== 公開管理介面 ==========
public function addWhitelist(string|array $ips): void
{
$this->whitelist = array_merge($this->whitelist, (array)$ips);
$this->compileRules();
}
public function addBlacklist(string|array $ips): void
{
$this->blacklist = array_merge($this->blacklist, (array)$ips);
$this->compileRules();
}
public function removeWhitelist(string $ip): void
{
$this->whitelist = array_values(array_diff($this->whitelist, [$ip]));
$this->compileRules();
}
public function getRules(): array
{
return ['whitelist' => $this->whitelist, 'blacklist' => $this->blacklist];
}
}
?>
<?php
// 設定檔或資料庫讀取
$config = [
'whitelist' => [
'192.168.1.0/24', // 內網區段
'10.0.*.*', // 萬用字元
'203.0.113.50', // 單一公網 IP
'2001:db8::/32', // IPv6 CIDR
],
'blacklist' => [
'192.168.1.100', // 封鎖內網特定主機
'198.51.100.0/24', // 封鎖惡意網段
],
'strict_proxy_check' => true,
];
$ipControl = new IpAccessControl($config);
// 中介層攔截器(放在所有業務邏輯之前)
if (!$ipControl->checkAccess()) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'Forbidden', 'message' => '您的 IP 不在允許清單或已被封鎖']);
exit;
}
// 正常業務邏輯...
echo "存取允許,您的 IP: " . $ipControl->getClientIp();
?>
應用層過濾雖靈活,但每個請求都需啟動 PHP。高流量建議在 Web 伺服器層封鎖:
# Nginx 範例
location /api/ {
allow 192.168.1.0/24;
allow 203.0.113.50;
deny all;
proxy_pass https://php_backend;
}
# Apache .htaccess 範例
Require ip 192.168.1.0/24
Require ip 203.0.113.50
Require not ip 192.168.1.100
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;,且 PHP 只信任來自已知負載平衡器的請求(可檢查 $_SERVER['REMOTE_ADDR'] 是否為內部 LB IP)。curl -6 驗證。$compiledRules 序列化存入 Redis/APCu,避免每請求重新編譯正則。建立完善的 IP 存取控制,是保護管理介面、API 端點、內部系統的第一道防線。