PHPBB 作為經典論壇系統,其封鎖管理模組(admin_user_ban.php)展示了完整的使用者存取控制實作。本文深度解析其核心邏輯,並提供現代化重構參考。
封鎖系統支援三種封鎖類型:
*@domain.com)支援 192.168.1.1-192.168.1.254 範圍格式,自動展開為離散 IP 列表:
<?php
function parseIpRange($ipRangeStr) {
$ipList = [];
$ranges = explode(',', $ipRangeStr);
foreach ($ranges as $range) {
$range = trim($range);
// 格式 1:IP 範圍 192.168.1.1-192.168.1.254
if (preg_match(
'/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\s*-\s*(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/',
$range, $m
)) {
$ipList = array_merge($ipList, expandIpRange(
[$m[1], $m[2], $m[3], $m[4]],
[$m[5], $m[6], $m[7], $m[8]]
));
}
// 格式 2:主機名稱 example.com
elseif (preg_match('/^([\w\-_]+\.?){2,}$/i', $range)) {
$ips = gethostbynamel($range);
if ($ips) $ipList = array_merge($ipList, $ips);
}
// 格式 3:通配符 192.168.*.*
elseif (preg_match('/^(\d{1,3})\.(\d{1,3}|\*)\.(\d{1,3}|\*)\.(\d{1,3}|\*)$/', $range, $m)) {
$ip = str_replace('*', '255', $range);
$ipList[] = encodeIp($ip);
}
}
return $ipList;
}
function expandIpRange($start, $end) {
$ips = [];
// 四層巢狀迴圈展開 IP 範圍(原始碼邏輯)
for ($o1 = $start[0]; $o1 <= $end[0]; $o1++) {
$o2Start = ($o1 == $start[0]) ? $start[1] : 0;
$o2End = ($o1 < $end[0]) ? 254 : $end[1];
for ($o2 = $o2Start; $o2 <= $o2End; $o2++) {
$o3Start = ($o1 == $start[0] && $o2 == $start[1]) ? $start[2] : 0;
$o3End = ($o1 < $end[0] || $o2 < $o2End) ? 254 : $end[2];
for ($o3 = $o3Start; $o3 <= $o3End; $o3++) {
$o4Start = ($o1 == $start[0] && $o2 == $start[1] && $o3 == $start[2]) ? $start[3] : 0;
$o4End = ($o1 < $end[0] || $o2 < $o2End || $o3 < $o3End) ? 254 : $end[3];
for ($o4 = $o4Start; $o4 <= $o4End; $o4++) {
$ips[] = encodeIp("$o1.$o2.$o3.$o4");
}
}
}
}
return $ips;
}
// IP 編碼函數(PHPBB 專用格式)
function encodeIp($ip) {
$parts = explode('.', $ip);
return sprintf('%02x%02x%02x%02x', $parts[0], $parts[1], $parts[2], $parts[3]);
}
?>
寫入資料庫後,立即刪除對應的 Session 強制登出:
<?php
function applyBans($userIds, $ipList, $emailList, $db) {
$killSessionSql = '';
// 1. 使用者封鎖
foreach ($userIds as $uid) {
if (!isAlreadyBanned('ban_userid', $uid, $db)) {
$db->query("INSERT INTO banlist (ban_userid) VALUES ($uid)");
$killSessionSql .= " OR session_user_id = $uid";
}
}
// 2. IP 封鎖
foreach ($ipList as $ip) {
if (!isAlreadyBanned('ban_ip', $ip, $db)) {
$db->query("INSERT INTO banlist (ban_ip) VALUES ('$ip')");
// 建立 Session 清理條件
if (preg_match('/(ff\.)|(\.ff)/i', chunk_split($ip, 2, '.'))) {
// 含通配符的 IP
$pattern = str_replace('.', '', preg_replace('/(ff\.)|(\.ff)/i', '%', chunk_split($ip, 2, '.')));
$killSessionSql .= " OR session_ip LIKE '$pattern'";
} else {
$killSessionSql .= " OR session_ip = '$ip'";
}
}
}
// 3. Email 封鎖
foreach ($emailList as $email) {
if (!isAlreadyBanned('ban_email', $email, $db)) {
$db->query("INSERT INTO banlist (ban_email) VALUES ('" . $db->escape($email) . "')");
}
}
// 4. 即時清理 Session
if ($killSessionSql) {
$db->query("DELETE FROM sessions WHERE " . substr($killSessionSql, 4));
}
}
function isAlreadyBanned($field, $value, $db) {
$result = $db->query("SELECT 1 FROM banlist WHERE $field = " . $db->escape($value));
return $db->num_rows($result) > 0;
}
?>
<?php
function removeBans($banIds, $db) {
if (empty($banIds)) return;
$placeholders = implode(',', array_fill(0, count($banIds), '?'));
$stmt = $db->prepare("DELETE FROM banlist WHERE ban_id IN ($placeholders)");
$stmt->execute($banIds);
}
?>
CREATE TABLE `banlist` (
`ban_id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`ban_type` ENUM('user', 'ip', 'email') NOT NULL,
`ban_value` VARCHAR(255) NOT NULL, -- 統一儲存值
`ban_reason` TEXT,
`ban_created_by` INT UNSIGNED,
`ban_created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`ban_expires_at` TIMESTAMP NULL, -- 支援臨時封鎖
INDEX `idx_type_value` (`ban_type`, `ban_value`),
INDEX `idx_expires` (`ban_expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
// 使用 PHP 內建 filter_var 驗證與標準化
function normalizeIp($input) {
// 單一 IP
if (filter_var($input, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $input . '/32';
}
// CIDR 格式
if (preg_match('/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/', $input)) {
return $input;
}
// 範圍格式轉 CIDR(需額外函式庫支援)
// ...
return false;
}
// 檢查 IP 是否在封鎖列表中
function isIpBanned($clientIp, $pdo) {
$stmt = $pdo->query("SELECT ban_value FROM banlist WHERE ban_type = 'ip'");
while ($row = $stmt->fetch()) {
if (ipInCidr($clientIp, $row['ban_value'])) return true;
}
return false;
}
function ipInCidr($ip, $cidr) {
list($subnet, $mask) = explode('/', $cidr);
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnet);
$maskLong = -1 << (32 - (int)$mask);
return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
}
?>
<?php
class BanMiddleware {
public function __construct(private PDO $pdo) {}
public function process(Request $request, RequestHandler $handler): Response {
$clientIp = $this->getClientIp($request);
$userId = $request->getAttribute('user_id') ?? null;
if ($this->isBanned($clientIp, $userId)) {
return new Response(403, [], '您的存取已被封鎖');
}
return $handler->handle($request);
}
private function getClientIp(Request $request): string {
// 支援代理標頭
$headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$ips = explode(',', $_SERVER[$header]);
return trim($ips[0]);
}
}
return '0.0.0.0';
}
private function isBanned(string $ip, ?int $userId): bool {
$stmt = $this->pdo->prepare("
SELECT 1 FROM banlist
WHERE (ban_type = 'ip' AND ? LIKE CONCAT(ban_value, '%'))
OR (ban_type = 'user' AND ban_value = ?)
AND (ban_expires_at IS NULL OR ban_expires_at > NOW())
");
$stmt->execute([$ip, $userId]);
return $stmt->fetchColumn() > 0;
}
}
?>
ban_expires_at)PHPBB 的封鎖機制雖然年代久遠,但其核心邏輯(IP 範圍展開、Session 即時失效、多類型封鎖)仍具參考價值。現代系統建議採用 CIDR 標準儲存 IP、整合中介軟體架構、支援臨時封鎖與自動過期,並搭配完整的審計追蹤與白名單保護機制。