首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 獲取客戶端 IP 與地理位置類:支援代理穿透、新浪介面、JSON 輸出

     編輯:PHP 相關知識

準確獲取客戶端真實 IP 並解析地理位置是風控、個性化推薦、日誌分析的基礎需求。本文提供生產級 IPLocator 類,支援多層代理穿透、IPv6、新浪 IP 介面、多種輸出格式、快取優化與錯誤處理。

核心功能

  • 智慧解析真實 IP:HTTP_X_FORWARDED_FOR、HTTP_X_REAL_IP、HTTP_CLIENT_IP、REMOTE_ADDR
  • 支援 IPv6 格式識別
  • 新浪 IP 歸屬地介面 (https://int.dpool.sina.com.cn/iplookup/iplookup.php)
  • 輸出格式:原始文字、JSON、PHP 陣列
  • 可選:僅回傳 IP、快取結果、自定義介面端點

完整類實現

<?php
/**
 * IP 地址獲取與地理位置解析
 * 依賴:curl、json、session (可選)
 */
class IPLocator
{
    private array $config = [
        'api_url' => 'https://int.dpool.sina.com.cn/iplookup/iplookup.php',
        'api_format' => 'json', // json, js
        'timeout' => 3.0,
        'cache_ttl' => 3600, // 秒,0 為不快取
        'cache_prefix' => 'ip_loc_',
        'trust_proxies' => true,
        'proxy_headers' => [
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
        ],
    ];
    
    private ?string $clientIp = null;
    private array $locationData = [];

    public function __construct(array $config = [])
    {
        $this->config = array_merge($this->config, $config);
    }

    /**
     * 獲取客戶端真實 IP
     */
    public function getClientIp(): string
    {
        if ($this->clientIp !== null) return $this->clientIp;
        
        if ($this->config['trust_proxies']) {
            foreach ($this->config['proxy_headers'] as $header) {
                if (!empty($_SERVER[$header])) {
                    // X-Forwarded-For 可能包含多個 IP,取第一個非私有 IP
                    $ips = array_map('trim', explode(',', $_SERVER[$header]));
                    foreach ($ips as $ip) {
                        if ($this->isValidPublicIp($ip)) {
                            return $this->clientIp = $ip;
                        }
                    }
                    // 都無效則取第一個
                    return $this->clientIp = $ips[0];
                }
            }
        }
        
        return $this->clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    }

    /**
     * 判斷是否為合法公網 IP (排除私有/保留段)
     */
    private function isValidPublicIp(string $ip): bool
    {
        if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;
        // 簡單過濾私有段,完整請用 ip2long/inet_pton 判斷
        $privateRanges = [
            '10.', '172.16.', '192.168.', '127.', '169.254.', '::1', 'fe80::', 'fc00:', 'fd00:'
        ];
        foreach ($privateRanges as $range) {
            if (str_starts_with($ip, $range)) return false;
        }
        return true;
    }

    /**
     * 獲取地理位置資料
     * @param string|null $ip 指定 IP,null 為自動獲取客戶端 IP
     * @param string $format 輸出格式: json, array, raw
     * @param bool $onlyIp 僅回傳 IP
     * @return mixed
     */
    public function getLocation(?string $ip = null, string $format = 'json', bool $onlyIp = false): mixed
    {
        $ip = $ip ?? $this->getClientIp();
        
        if ($onlyIp) {
            return $this->formatOutput($ip, $format, true);
        }
        
        // 快取檢查
        $cacheKey = $this->config['cache_prefix'] . md5($ip);
        if ($this->config['cache_ttl'] > 0 && isset($_SESSION[$cacheKey])) {
            $cached = $_SESSION[$cacheKey];
            if (time() - $cached['time'] < $this->config['cache_ttl']) {
                return $this->formatOutput($cached['data'], $format);
            }
        }
        
        // 呼叫新浪介面
        $url = $this->config['api_url'] . '?ip=' . urlencode($ip) . '&format=' . $this->config['api_format'];
        $result = $this->httpGet($url);
        
        if ($result === false) {
            return $this->formatOutput(['ip' => $ip, 'error' => 'api_request_failed'], $format);
        }
        
        // 解析回應
        $data = $this->parseResponse($result, $ip);
        
        // 寫入快取
        if ($this->config['cache_ttl'] > 0) {
            $_SESSION[$cacheKey] = ['time' => time(), 'data' => $data];
        }
        
        return $this->formatOutput($data, $format);
    }

    private function httpGet(string $url): string|false
    {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT_MS => (int)($this->config['timeout'] * 1000),
            CURLOPT_CONNECTTIMEOUT_MS => 1000,
            CURLOPT_SSL_VERIFYPEER => false, // 新浪介面 HTTP
            CURLOPT_USERAGENT => 'IPLocator/1.0',
        ]);
        $response = curl_exec($ch);
        $errno = curl_errno($ch);
        curl_close($ch);
        return $errno ? false : $response;
    }

    private function parseResponse(string $response, string $ip): array
    {
        // 新浪 JSON 格式: {"ret":1,"start":"...","end":"...","country":"中國","province":"北京","city":"北京","district":"","isp":"","type":"","desc":"..."}
        $data = json_decode($response, true);
        if (json_last_error() === JSON_ERROR_NONE && isset($data['ret']) && $data['ret'] == 1) {
            return [
                'ip' => $ip,
                'country' => $data['country'] ?? '',
                'province' => $data['province'] ?? '',
                'city' => $data['city'] ?? '',
                'district' => $data['district'] ?? '',
                'isp' => $data['isp'] ?? '',
                'raw' => $data
            ];
        }
        
        // JS 格式回退解析
        // var remote_ip_info = {"ret":1,"country":"中國",...};
        if (preg_match('/\{.*\}/', $response, $m)) {
            $data = json_decode($m[0], true);
            if (json_last_error() === JSON_ERROR_NONE) {
                return [
                    'ip' => $ip,
                    'country' => $data['country'] ?? '',
                    'province' => $data['province'] ?? '',
                    'city' => $data['city'] ?? '',
                    'district' => $data['district'] ?? '',
                    'isp' => $data['isp'] ?? '',
                    'raw' => $data
                ];
            }
        }
        
        return ['ip' => $ip, 'error' => 'parse_failed', 'raw_response' => $response];
    }

    private function formatOutput(mixed $data, string $format, bool $onlyIp = false): mixed
    {
        if ($onlyIp && is_string($data)) return $data;
        
        return match (strtolower($format)) {
            'json' => json_encode($data, JSON_UNESCAPED_UNICODE),
            'array' => is_array($data) ? $data : ['ip' => $data],
            'raw' => $data,
            default => $data
        };
    }
    
    // 靜態快捷方法
    public static function getClientIp(array $config = []): string
    {
        return (new self($config))->getClientIp();
    }
    
    public static function getLocation(?string $ip = null, array $config = [], string $format = 'json'): mixed
    {
        return (new self($config))->getLocation($ip, $format);
    }
}

使用範例

1. 基本用法 (自動獲取當前訪客)

<?php
session_start(); // 啟用快取需 session
require 'IPLocator.php';

// 僅獲取 IP
$ip = IPLocator::getClientIp();
echo "Client IP: $ip\n";

// 獲取地理位置 (JSON)
$location = IPLocator::getLocation();
echo $location;
// {"ip":"203.208.60.12","country":"中國","province":"北京","city":"北京","district":"","isp":"電信",...}

// 獲取 PHP 陣列
$arr = IPLocator::getLocation(format: 'array');
print_r($arr);

2. 指定 IP 查詢 (後台管理、日誌分析)

$ips = ['8.8.8.8', '1.1.1.1', '203.208.60.12'];
foreach ($ips as $ip) {
    $loc = IPLocator::getLocation($ip, format: 'array');
    echo "{$loc['ip']} => {$loc['country']} {$loc['province']} {$loc['city']} {$loc['isp']}\n";
}

3. 前端 JavaScript 整合 (AJAX)

<!-- api/ip.php -->
<?php
session_start();
require '../IPLocator.php';
header('Content-Type: application/json; charset=utf-8');
$loc = IPLocator::getLocation(format: 'json');
echo $loc;
?>

// 前端
fetch('/api/ip.php')
  .then(r => r.json())
  .then(data => {
    console.log('用戶位置:', data.country, data.province, data.city);
    // 根據地理位置顯示不同內容、幣別、語言
  });

4. 自定義配置 (更換介面、調整超時、關閉快取)

$locator = new IPLocator([
    'api_url' => 'https://api.ip.sb/geoip/', // 替代介面
    'api_format' => 'json',
    'timeout' => 5.0,
    'cache_ttl' => 0, // 關閉快取
    'trust_proxies' => true,
]);
$location = $locator->getLocation('8.8.8.8', 'array');
print_r($location);

新浪介面回應格式對照

欄位含義範例
ret狀態碼 (1=成功)1
country國家中國
province省份/直轄市北京
city城市北京
district區/縣海淀區
isp運營商電信/聯通/移動/鐵通/教育網
type線路類型學校/公司/家庭
desc描述北京市海淀區 電信

替代 IP 歸屬地介面推薦

服務介面特點限制
ip.sbhttps://api.ip.sb/geoip/{ip}免費、HTTPS、支援 IPv6、豐富欄位頻率限制
ip-api.comhttps://ip-api.com/json/{ip}免費、詳盡、支援批次非商業、HTTP、45 req/min
ipinfo.iohttps://ipinfo.io/{ip}/json免費額度大、企業級需 Token、付費進階
GeoIP2 (MaxMind)本地資料庫 (MMDB)離線、高效能、精準需定期更新資料庫

生產環境部署建議

  1. 改用本地 GeoIP2 資料庫:安裝 php-maxmind-db 擴展,零網路延遲、高併發、隱私合規
  2. Redis 快取替代 Session:分散式部署時共享快取,SETEX ip_loc:{md5(ip)} 3600 {json}
  3. 代理鏈信任配置:明確配置 trust_proxies 與代理 Header,防止 IP 偽造
  4. IPv6 支援:驗證 isValidPublicIp 正確處理 IPv6 格式
  5. 監控介面可用性:記錄 API 失敗率、延遲,設定告警

這個 IPLocator 類提供了從簡單到生產級的完整演進路徑。起步用新浪介面零成本,量大遷移至本地 GeoIP2,架構層面保持介面不變,業務代碼零修改。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved