原文 strcmp 比較法 (有缺陷)
<?php
$IP = $_SERVER['REMOTE_ADDR'];
$from = strcmp($IP, '192.168.0.0');
$to = strcmp($IP, '192.168.0.255');
if (!($from >= 0 && $to <= 0)) {
echo "Access Denied";
} else {
echo "Homepage";
}
為何 strcmp 比較 IP 不可靠
<?php
// 字串比較 vs 數值比較
var_dump(strcmp('192.168.0.5', '192.168.0.255')); // int(-1) 正確
var_dump(strcmp('192.168.0.100', '192.168.0.255'));// int(-1) 正確
var_dump(strcmp('192.168.1.5', '192.168.0.255')); // int(1) 錯誤!1.5 > 0.255 但實際 IP 較大
// 根本原因:字串逐字元比較,'1' > '0' 導致 192.168.1.x 被判定大於 192.168.0.255
正確解法一:ip2long 數值比較 (IPv4 專用)
<?php
function ipInRange($ip, $startIp, $endIp) {
$ip = ip2long($ip);
$start = ip2long($startIp);
$end = ip2long($endIp);
if ($ip === false || $start === false || $end === false) return false;
return ($ip >= $start && $ip <= $end);
}
// 使用
if (!ipInRange($_SERVER['REMOTE_ADDR'], '192.168.0.0', '192.168.0.255')) {
header('HTTP/1.1 403 Forbidden');
exit('Access Denied');
}
正確解法二:CIDR 表示法 (推薦、支援 IPv6)
<?php
function ipInCidr($ip, $cidr) {
list($subnet, $mask) = explode('/', $cidr);
$mask = $mask ?: 32;
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = ~((1 << (32 - $mask)) - 1);
return ($ip & $mask) === ($subnet & $mask);
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// IPv6 需用 inet_pton 二進位比較
$ip = inet_pton($ip);
$subnet = inet_pton($subnet);
$mask = str_repeat('f', $mask / 4) . str_repeat('0', 32 - $mask / 4);
// 簡化:建議直接用 套件
return false; // 此處略
}
return false;
}
// 使用:192.168.0.0/24 代表 192.168.0.0 - 192.168.0.255
if (!ipInCidr($_SERVER['REMOTE_ADDR'], '192.168.0.0/24')) {
exit('Access Denied');
}
正確解法三:使用成熟套件 (生產環境首選)
composer require ramsey/ip
use Ramsey\Ip\IpRange;
$range = IpRange::fromCidr('192.168.0.0/24');
if (!$range->contains($_SERVER['REMOTE_ADDR'])) {
http_response_code(403);
exit('Access Denied');
}
完整存取控制中介層範例
<?php
class IpAccessControl {
private $allowList = [];
private $denyList = [];
public function __construct(array $config) {
$this->allowList = $config['allow'] ?? [];
$this->denyList = $config['deny'] ?? [];
}
public function check() {
$ip = $this->getClientIp();
// 拒絕名單優先
foreach ($this->denyList as $cidr) {
if ($this->match($ip, $cidr)) return false;
}
// 允許名單為空 = 全部允許 (除非被拒絕)
if (empty($this->allowList)) return true;
foreach ($this->allowList as $cidr) {
if ($this->match($ip, $cidr)) return true;
}
return false;
}
private function getClientIp() {
// 同前文:支援 X-Forwarded-For、真實 IP 取得
$headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'];
foreach ($headers as $h) {
if (!empty($_SERVER[$h]) && $_SERVER[$h] !== 'unknown') {
$ips = explode(',', $_SERVER[$h]);
return trim($ips[0]);
}
}
return '0.0.0.0';
}
private function match($ip, $cidr) {
// 使用 ramsey/ip 或自行實作 CIDR 比對
return IpRange::fromCidr($cidr)->contains($ip);
}
}
// 使用
$config = [
'allow' => ['192.168.0.0/24', '10.0.0.0/8'],
'deny' => ['192.168.0.100/32'], // 具體 IP 封鎖
];
$guard = new IpAccessControl($config);
if (!$guard->check()) {
http_response_code(403);
include '403.php';
exit;
}
跳轉 vs 直接輸出 vs 包含檔案
| 方式 | 程式碼 | 適用場景 |
| 直接輸出 | echo "Access Denied"; | API、AJAX 端點 |
| 包含頁面 | include 'homepage.html'; | 傳統 PHP 頁面、SEO 需求 |
| HTTP 跳轉 | header('Location: /403.html'); | 統一錯誤頁、記錄追蹤 |
| JSON 回應 | header('Content-Type: application/json'); echo json_encode(['error'=>'forbidden']); | REST API |