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

PHP 實現後台登入單機在線限制:防止帳號共用

     編輯:PHP 相關知識

需求場景

後台管理系統要求:同一帳號同一時間僅允許一台設備/瀏覽器在線。後登入者踢下線前者,或拒絕後登入。常見於付費會員、高權限後台、考試系統。

核心設計思路

  1. 登入成功 → 生成唯一 Token(Session ID 或自定義),將 user_id + token + IP + 登入時間 寫入「在線登記表」(Redis/資料庫/檔案)。
  2. 每次請求驗證 → 檢查當前 Session Token 是否與登記表一致;不一致 → 強制登出/提示「帳號在別處登入」。
  3. 心跳/活躍檢查 → 定期更新「最後活躍時間」,超時自動清理殘留登記。
  4. 登出/過期 → 移除登記表記錄。

方案一:Redis 實現(推薦生產環境)

資料結構

# Key: online:user:{userId}  Value: JSON {token, ip, login_at, last_active}
# TTL: 30 分鐘(滑動過期)

核心代碼

<?php
class SingleLoginManager {
    private \Redis $redis;
    private int $ttl = 1800; // 30 分鐘

    public function __construct(\Redis $redis) {
        $this->redis = $redis;
    }

    // 登入時調用:返回 [success, message, token]
    public function login(int $userId, string $ip): array {
        $token = bin2hex(random_bytes(32)); // 高強度隨機 Token
        $key = "online:user:{$userId}";

        // 檢查是否已有其他設備在線
        $existing = $this->redis->get($key);
        if ($existing) {
            $old = json_decode($existing, true);
            // 可選策略:1. 踢下舊設備  2. 拒絕新登入
            // 這裡採用「踢下舊設備」
            $this->redis->del("session:{$old['token']}"); // 刪除舊 Session(若 Session 存 Redis)
        }

        // 寫入新登入記錄
        $data = [
            'token' => $token,
            'ip' => $ip,
            'login_at' => time(),
            'last_active' => time()
        ];
        $this->redis->setex($key, $this->ttl, json_encode($data));
        // 同時建立反向映射(用於快速驗證)
        $this->redis->setex("session:{$token}", $this->ttl, (string)$userId);

        return [true, '登入成功', $token];
    }

    // 每次請求驗證(中介軟體調用)
    public function validate(int $userId, string $token): bool {
        $key = "online:user:{$userId}";
        $data = $this->redis->get($key);
        if (!$data) return false; // 無登入記錄

        $info = json_decode($data, true);
        if ($info['token'] !== $token) return false; // Token 不匹配 = 別處登入

        // 更新最後活躍時間,續期 TTL
        $info['last_active'] = time();
        $this->redis->setex($key, $this->ttl, json_encode($info));
        $this->redis->expire("session:{$token}", $this->ttl);

        return true;
    }

    // 登出調用
    public function logout(int $userId, string $token): void {
        $this->redis->del("online:user:{$userId}");
        $this->redis->del("session:{$token}");
    }

    // 強制踢下線(管理員操作)
    public function forceLogout(int $userId): void {
        $data = $this->redis->get("online:user:{$userId}");
        if ($data) {
            $info = json_decode($data, true);
            $this->redis->del("session:{$info['token']}");
        }
        $this->redis->del("online:user:{$userId}");
    }
}
?>

方案二:資料庫 + 檔案鎖(無 Redis 環境)

<?php
// 資料表:user_online (user_id PK, token, ip, login_at, last_active)
// 登入
function doLogin(PDO $pdo, int $userId, string $ip): array {
    $token = bin2hex(random_bytes(32));
    $pdo->beginTransaction();
    try {
        // 排他鎖防併發
        $stmt = $pdo->prepare("SELECT token FROM user_online WHERE user_id = ? FOR UPDATE");
        $stmt->execute([$userId]);
        if ($row = $stmt->fetch()) {
            // 刪除舊 Session(假設 Session 存檔案/資料庫)
            deleteSession($row['token']);
        }
        $stmt = $pdo->prepare("REPLACE INTO user_online (user_id, token, ip, login_at, last_active) VALUES (?, ?, ?, NOW(), NOW())");
        $stmt->execute([$userId, $token, $ip]);
        $pdo->commit();
        return [true, $token];
    } catch (Exception $e) {
        $pdo->rollBack();
        return [false, $e->getMessage()];
    }
}

// 驗證(每請求調用)
function validateOnline(PDO $pdo, int $userId, string $token): bool {
    $stmt = $pdo->prepare("SELECT token, last_active FROM user_online WHERE user_id = ?");
    $stmt->execute([$userId]);
    $row = $stmt->fetch();
    if (!$row || $row['token'] !== $token) return false;
    // 更新活躍時間
    $pdo->prepare("UPDATE user_online SET last_active = NOW() WHERE user_id = ?")->execute([$userId]);
    return true;
}
?>

前端配合:偵測被踢下線

<script>
// 定期輪詢 /heartbeat 檢查登入狀態
setInterval(async () => {
    const res = await fetch('/api/heartbeat');
    const data = await res.json();
    if (data.code === 401 && data.message.includes('別處登入')) {
        alert('您的帳號已在其他設備登入,當前會話將結束。');
        window.location.href = '/login?kicked=1';
    }
}, 30000); // 每 30 秒檢查一次
</script>

進階考量

議題建議
同一瀏覽器多標籤共享 Session Cookie,視為同一設備,不互踢
無痕模式/不同瀏覽器不同 Cookie → 不同 Session → 觸發單機限制
負載均衡多節點**必須**使用共享存儲,不可用本地檔案/記憶體
Session 與 Token 綁定將 Token 存入 Session:$_SESSION['single_token'] = $token,驗證時比對
記住我(長期登入)長期 Token 另存 remember_tokens 表,單機限制僅對當前活躍 Session 生效

結語

單機在線限制的核心是 「唯一 Token + 共享存儲 + 每請求驗證」。Redis 方案效能最佳、代碼最簡;資料庫方案通用性強。切記:將驗證邏輯封裝為中介軟體/過濾器,全局攔截,避免在每個控制器重複代碼。

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