Web 開發中重複處理 $_GET/$_POST/$_COOKIE/$_SERVER 既繁瑣又易出錯。本文提供一個生產級輸入處理類 Ev_Input,整合路由解析、資料清洗、XSS 防護、SQL 注入預防、IP 獲取、二級域名識別等核心功能。
<?php
class Ev_Input {
public $cookie = [];
public $post = [];
public $get = [];
public $file = [];
public $url = []; // 路由解析結果
public $G = []; // 別名
private $e = []; // 快取 (IP等)
public function __construct() {
// 1. 處理 magic_quotes_gpc (PHP 5.4+ 已移除,保留相容)
if (ini_get('magic_quotes_gpc')) {
$get = $this->stripSlashes($_REQUEST);
$post = $this->stripSlashes($_POST);
$this->cookie = $this->stripSlashes($_COOKIE);
} else {
$get = $_REQUEST;
$post = $_POST;
$this->cookie = $_COOKIE;
}
$this->file = $_FILES;
$this->get = $this->initData($get);
$this->post = $this->initData($post);
$this->url = $this->parseUrl();
$this->cookie = $this->initData($this->cookie);
$this->G = &$this->get; // 別名引用
}
// 路由解析:支援 route 參數 或 QUERY_STRING
private function parseUrl(): array {
if (isset($_REQUEST['route'])) {
return explode('-', $_REQUEST['route']); // 如 controller-action-param
} elseif (isset($_SERVER['QUERY_STRING'])) {
$qs = $_SERVER['QUERY_STRING'];
$qs = explode('#', $qs, 2)[0]; // 去除錨點
$qs = explode('&', $qs, 2)[0]; // 取第一個參數
return explode('-', $qs);
}
return [];
}
// 獲取 GET 參數
public function get(string $key, $default = null) {
return $this->get[$key] ?? $default;
}
// 獲取 POST 參數
public function post(string $key, $default = null) {
return $this->post[$key] ?? $default;
}
// 獲取路由段 (索引從 0 開始)
public function url(int $index, $default = null) {
return $this->url[$index] ?? $default;
}
// Cookie 管理
public function setCookie(string $name, string $value, int $expire = 3600): void {
$expire = $expire ? time() + $expire : 0;
$prefix = defined('COOKIE_PREFIX') ? COOKIE_PREFIX : '';
$domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '';
setcookie($prefix . $name, $value, $expire, '/', $domain, false, true);
}
public function getCookie(string $key, bool $noPrefix = false, $default = null) {
$prefix = defined('COOKIE_PREFIX') ? COOKIE_PREFIX : '';
$key = $noPrefix ? $key : $prefix . $key;
return $this->cookie[$key] ?? $default;
}
public function getFile(string $key) {
return $this->file[$key] ?? null;
}
// 核心:資料遞歸清洗
private function initData($data) {
if (is_array($data)) {
foreach ($data as $k => $v) {
// 鍵名白名單檢查 (防污染)
if (!$this->isAllowKey($k)) {
unset($data[$k]);
continue;
}
$data[$k] = $this->initData($v);
}
return $data;
}
// 純量值處理
if (is_numeric($data)) {
// 防止 08/09 被當作八進制、長數字科學計數法
if ($data[0] === '0' || strlen($data) >= 11) {
return $this->addSlashes(htmlspecialchars(str_replace("'", ''', $data)));
}
return strpos($data, '.') !== false ? (float)$data : (int)$data;
}
if (is_string($data)) {
return $this->addSlashes(htmlspecialchars(str_replace("'", ''', $data)));
}
if (is_bool($data)) return (bool)$data;
return $data; // null, resource 等保持原樣
}
// 鍵名白名單 (防止 __proto__、constructor 等原型鏈污染)
private function isAllowKey(string $key): bool {
// 僅允許字母數字下劃線,長度限制
return preg_match('/^[a-zA-Z0-9_]{1,64}$/', $key) === 1;
}
// 遞歸去除魔術引號
private function stripSlashes($data) {
if (is_array($data)) {
foreach ($data as $k => $v) $data[$k] = $this->stripSlashes($v);
} else {
$data = stripslashes(trim($data));
}
return $data;
}
// 遞歸添加轉義 (資料庫安全)
private function addSlashes($data) {
if (is_array($data)) {
foreach ($data as $k => $v) $data[$k] = $this->addSlashes($v);
} else {
$data = addslashes(trim($data));
}
return $data;
}
// 獲取客戶端真實 IP
public function getClientIp(): string {
if (!isset($this->e['ip'])) {
$ip = 'unknown';
$headers = [
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'REMOTE_ADDR'
];
foreach ($headers as $header) {
$val = getenv($header) ?: ($_SERVER[$header] ?? '');
if ($val && strcasecmp($val, 'unknown') !== 0) {
// X-Forwarded-For 可能包含多個 IP,取第一個非私有
$ips = array_map('trim', explode(',', $val));
foreach ($ips as $ip) {
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) continue;
$this->e['ip'] = $ip;
break 2;
}
}
}
$this->e['ip'] = $this->e['ip'] ?? 'unknown';
}
return $this->e['ip'];
}
// 獲取二級域名 (如 user.example.com → user)
public function getSecondDomain(): ?string {
$host = $_SERVER['HTTP_HOST'] ?? '';
// 處理雙後綴域名
$host = preg_replace('/\.(com|net|org|gov|edu)\.cn$/', '.cn', $host);
$parts = explode('.', $host);
if (count($parts) < 3) return null;
$sub = $parts[0];
return is_numeric($sub) ? null : $sub; // 排除純數字子域名
}
}
// 全域單例模式使用
$ev = new Ev_Input();
// 控制器中
$userId = $ev->post('user_id');
$action = $ev->url(1); // 第二個路由段
$token = $ev->getCookie('token');
$ip = $ev->getClientIp();
$ev->get()/$ev->post(),便於測試 Mock、遷移 CLI/Swoolemagic_quotes 清洗、路由解析、資料初始化initData()| 輸入類型 | 處理邏輯 | 輸出 |
|---|---|---|
| 陣列 | 遞歸處理、鍵名白名單過濾 | 清洗後陣列 |
| 純數字字串 (含前導 0/長度≥11) | 視為字串 → htmlspecialchars + addslashes | 安全字串 |
| 短數字 | 轉換為 int/float | 原生數值型別 |
| 字串 | htmlspecialchars (防 XSS) + addslashes (防 SQL 注入) + 單引號實體化 | 安全字串 |
| 布林 | 強制轉型 | bool |
注意:雙重防護 (htmlspecialchars + addslashes) 適用於**直接拼接 SQL 的舊專案**。現代專案請使用 **PDO 預處理**,移除 addslashes,僅保留輸出時的 htmlspecialchars。
parseUrl()?route=controller-action-id 模式QUERY_STRING 首段['controller', 'action', 'id'],配合調度器使用COOKIE_PREFIX (如 myapp_) 避免多子系統 Cookie 衝突setCookie/getCookie 自動加前綴,支援 COOKIE_DOMAIN 跨子域共享HttpOnly=true, Secure=true (HTTPS 環境)X-Forwarded-For 多 IP 解析,過濾私有/保留地址Client-IP → X-Forwarded-For → X-Real-IP → REMOTE_ADDR$this->e['ip'] 避免重複解析tenant.app.com → 識別 tenant.com.cn 等雙後綴// 1. 型別宣告與嚴格模式
declare(strict_types=1);
class Ev_Input {
// 2. 唯讀屬性 (PHP 8.1+)
public readonly array $get;
public readonly array $post;
// 3. 建構子屬性提升
public function __construct(
private array $server = [],
private array $get = [],
private array $post = [],
private array $cookie = [],
private array $files = []
) {
// 依賴注入,便於測試
$this->server = $server ?: $_SERVER;
// ...
}
// 4. 使用 filter_var 驗證替代正則
private function isAllowKey(string $key): bool {
return strlen($key) <= 64 && ctype_alnum(str_replace('_', '', $key));
}
// 5. 移除 addslashes,改用 PDO::quote 或預處理
private function sanitizeString(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
}
| 場景 | 建議做法 |
|---|---|
| 舊專案維護 (mysqli/mysql 拼接 SQL) | 直接使用本類,雙重防護 |
| 現代專案 (PDO/Doctrine/Eloquent) | 移除 addslashes,僅保留 htmlspecialchars 輸出端 |
| API 服務 (JSON 輸入) | 擴充套件支援 php://input JSON 解析 |
| Swoole/RoadRunner 長連線 | 每請求 new 實例,避免屬性殘留 |
| 單元測試 | 構造函數注入模擬 $_GET/$_POST |
封裝輸入處理不僅是代碼復用,更是建立**安全邊界**的關鍵一環。統一入口、白名單鍵名、輸出編碼、最小權限原則,讓業務代碼專注於邏輯而非防禦。