[PHP]session_id関数を完全解説!セッションIDを取得・設定する方法

PHP

こんにちは!今回は、PHPの標準関数であるsession_id()について詳しく解説していきます。セッションIDの取得と設定ができる、セッション管理の基本となる重要な関数です!

session_id関数とは?

session_id()関数は、現在のセッションIDを取得、または新しいセッションIDを設定する関数です。

セッション開始前にIDを設定したり、開始後に現在のIDを確認したりできます。セッション固定攻撃対策やマルチデバイス管理などに使用されます!

基本的な構文

session_id(?string $id = null): string|false
  • $id: 設定する新しいセッションID(省略時は取得のみ)
  • 戻り値: 現在のセッションID、失敗時はfalse

重要な注意点

// セッション開始前に設定、開始後に取得

// 取得(セッション開始後)
session_start();
$sessionId = session_id();
echo "現在のセッションID: {$sessionId}\n";

// 設定(セッション開始前)
$newId = 'custom_session_id_12345';
session_id($newId);
session_start();
echo "設定したID: " . session_id() . "\n";

// セッション開始後の設定は無効
session_start();
session_id('new_id');  // 効果なし
echo session_id();     // 元のIDのまま

// セッションIDの形式制限
// 英数字とカンマ(,)、ハイフン(-)のみ使用可能
// 無効な文字を含むとエラー

// 空文字列でセッションIDをクリア(開始前のみ)
session_id('');
session_start();  // 新しいIDが自動生成される

// session_regenerate_id()との違い
session_start();
session_regenerate_id();  // セッション継続中にIDを再生成(推奨)
// vs
session_destroy();
session_id('new_id');
session_start();  // 完全に新しいセッション

セッションIDの特性

// セッションIDの長さと形式

session_start();
$id = session_id();

echo "セッションID: {$id}\n";
echo "長さ: " . strlen($id) . " 文字\n";

// デフォルトの長さは設定により異なる
// session.sid_length (PHP 7.1.0以降、デフォルト32)
// session.hash_bits_per_character (デフォルト4)

echo "sid_length: " . ini_get('session.sid_length') . "\n";
echo "hash_bits_per_character: " . ini_get('session.hash_bits_per_character') . "\n";

// セッションIDの一意性
// PHPが自動生成するIDは暗号的に安全な乱数
// 重複の可能性は極めて低い

// セッションIDとファイル名の関係
$sessionPath = session_save_path();
if (empty($sessionPath)) {
    $sessionPath = sys_get_temp_dir();
}
$filename = $sessionPath . '/sess_' . session_id();
echo "セッションファイル: {$filename}\n";

基本的な使用例

セッションIDの取得

// セッション開始
session_start();

// 現在のセッションIDを取得
$sessionId = session_id();

echo "セッションID: {$sessionId}\n";
echo "ID長: " . strlen($sessionId) . " 文字\n";

// セッション名も確認
$sessionName = session_name();
echo "セッション名: {$sessionName}\n";

// クッキーに保存されているか確認
if (isset($_COOKIE[$sessionName])) {
    echo "クッキーのID: {$_COOKIE[$sessionName]}\n";
    echo "一致: " . ($sessionId === $_COOKIE[$sessionName] ? 'Yes' : 'No') . "\n";
}

カスタムセッションIDの設定

// カスタムIDを設定(セッション開始前)
$customId = bin2hex(random_bytes(16));  // 32文字の16進数
echo "カスタムID: {$customId}\n";

session_id($customId);
session_start();

// 設定されたか確認
echo "現在のID: " . session_id() . "\n";
echo "一致: " . ($customId === session_id() ? 'Yes' : 'No') . "\n";

セッションIDの検証

// セッションIDの形式を検証
function validateSessionId($id) {
    // 長さチェック(通常22-128文字)
    if (strlen($id) < 22 || strlen($id) > 128) {
        return false;
    }
    
    // 文字チェック(英数字、カンマ、ハイフン)
    if (!preg_match('/^[a-zA-Z0-9,-]+$/', $id)) {
        return false;
    }
    
    return true;
}

// テスト
$validId = session_id();
$invalidId = 'invalid@id#123';

echo "Valid ID: " . ($validId ? 'Yes' : 'No') . " - " . 
     (validateSessionId($validId) ? 'OK' : 'NG') . "\n";
echo "Invalid ID: " . (validateSessionId($invalidId) ? 'OK' : 'NG') . "\n";

セッションIDの比較

session_start();
$id1 = session_id();

// セッションを終了
session_write_close();

// 新しいセッションを開始
session_start();
$id2 = session_id();

echo "1回目のID: {$id1}\n";
echo "2回目のID: {$id2}\n";
echo "同じID: " . ($id1 === $id2 ? 'Yes' : 'No') . "\n";
// 通常は同じID(クッキーが保持されている場合)

実践的な使用例

例1: セッションID管理システム

class SessionIdManager {
    private $idHistory = [];
    
    /**
     * 現在のセッションIDを取得
     */
    public function getCurrentId() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $id = session_id();
        
        return [
            'session_id' => $id,
            'length' => strlen($id),
            'session_name' => session_name(),
            'session_status' => $this->getStatusName(session_status()),
            'created_at' => $_SESSION['created_at'] ?? null
        ];
    }
    
    /**
     * セッションステータス名を取得
     */
    private function getStatusName($status) {
        switch ($status) {
            case PHP_SESSION_DISABLED:
                return 'disabled';
            case PHP_SESSION_NONE:
                return 'none';
            case PHP_SESSION_ACTIVE:
                return 'active';
            default:
                return 'unknown';
        }
    }
    
    /**
     * セッションIDを再生成
     */
    public function regenerateId($deleteOldSession = true) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $oldId = session_id();
        
        // 再生成
        session_regenerate_id($deleteOldSession);
        
        $newId = session_id();
        
        // 履歴に記録
        $this->idHistory[] = [
            'old_id' => $oldId,
            'new_id' => $newId,
            'timestamp' => time(),
            'delete_old' => $deleteOldSession
        ];
        
        // 再生成時刻を記録
        $_SESSION['last_regenerated'] = time();
        
        return [
            'old_id' => $oldId,
            'new_id' => $newId,
            'regenerated_at' => time()
        ];
    }
    
    /**
     * カスタムセッションIDで開始
     */
    public function startWithCustomId($customId = null) {
        if (session_status() === PHP_SESSION_ACTIVE) {
            throw new Exception("Session already started");
        }
        
        // カスタムIDが指定されていない場合は生成
        if ($customId === null) {
            $customId = $this->generateSecureId();
        }
        
        // バリデーション
        if (!$this->validateId($customId)) {
            throw new Exception("Invalid session ID format");
        }
        
        session_id($customId);
        session_start();
        
        $_SESSION['created_at'] = time();
        $_SESSION['custom_id'] = true;
        
        return [
            'session_id' => session_id(),
            'custom' => true,
            'started_at' => time()
        ];
    }
    
    /**
     * セキュアなセッションIDを生成
     */
    public function generateSecureId($length = 32) {
        // 暗号的に安全な乱数を生成
        $bytes = random_bytes($length);
        return bin2hex($bytes);
    }
    
    /**
     * セッションIDを検証
     */
    public function validateId($id) {
        // 長さチェック
        $minLength = 22;
        $maxLength = 128;
        
        if (strlen($id) < $minLength || strlen($id) > $maxLength) {
            return false;
        }
        
        // 文字チェック(英数字、カンマ、ハイフン)
        if (!preg_match('/^[a-zA-Z0-9,-]+$/', $id)) {
            return false;
        }
        
        return true;
    }
    
    /**
     * セッションID情報を取得
     */
    public function getIdInfo() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            return [
                'error' => 'Session not active',
                'status' => 'inactive'
            ];
        }
        
        $id = session_id();
        
        return [
            'id' => $id,
            'length' => strlen($id),
            'valid' => $this->validateId($id),
            'entropy' => $this->calculateEntropy($id),
            'age' => isset($_SESSION['created_at']) ? time() - $_SESSION['created_at'] : null,
            'regeneration_count' => count($this->idHistory),
            'last_regenerated' => $_SESSION['last_regenerated'] ?? null
        ];
    }
    
    /**
     * エントロピーを計算(簡易版)
     */
    private function calculateEntropy($string) {
        $chars = str_split($string);
        $frequency = array_count_values($chars);
        $length = strlen($string);
        
        $entropy = 0;
        foreach ($frequency as $count) {
            $p = $count / $length;
            $entropy -= $p * log($p, 2);
        }
        
        return round($entropy, 2);
    }
    
    /**
     * セッションファイルのパスを取得
     */
    public function getSessionFilePath() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $sessionPath = session_save_path();
        if (empty($sessionPath)) {
            $sessionPath = sys_get_temp_dir();
        }
        
        $id = session_id();
        $filePath = $sessionPath . '/sess_' . $id;
        
        return [
            'directory' => $sessionPath,
            'filename' => 'sess_' . $id,
            'full_path' => $filePath,
            'exists' => file_exists($filePath),
            'size' => file_exists($filePath) ? filesize($filePath) : null,
            'modified' => file_exists($filePath) ? filemtime($filePath) : null
        ];
    }
    
    /**
     * IDの履歴を取得
     */
    public function getHistory() {
        return $this->idHistory;
    }
    
    /**
     * セッションIDを比較
     */
    public function compareIds($id1, $id2) {
        return [
            'id1' => $id1,
            'id2' => $id2,
            'identical' => $id1 === $id2,
            'length_match' => strlen($id1) === strlen($id2),
            'similarity' => similar_text($id1, $id2, $percent),
            'similarity_percent' => round($percent, 2)
        ];
    }
    
    /**
     * レポートを生成
     */
    public function generateReport() {
        $current = $this->getCurrentId();
        $info = $this->getIdInfo();
        $file = $this->getSessionFilePath();
        
        $report = "=== Session ID Report ===\n\n";
        $report .= "Current ID: {$current['session_id']}\n";
        $report .= "Length: {$current['length']} characters\n";
        $report .= "Status: {$current['session_status']}\n";
        $report .= "Valid: " . ($info['valid'] ? 'Yes' : 'No') . "\n";
        $report .= "Entropy: {$info['entropy']}\n";
        
        if ($info['age'] !== null) {
            $report .= "Age: {$info['age']} seconds\n";
        }
        
        $report .= "\nSession File:\n";
        $report .= "Path: {$file['full_path']}\n";
        $report .= "Exists: " . ($file['exists'] ? 'Yes' : 'No') . "\n";
        
        if ($file['exists']) {
            $report .= "Size: {$file['size']} bytes\n";
            $report .= "Modified: " . date('Y-m-d H:i:s', $file['modified']) . "\n";
        }
        
        if (!empty($this->idHistory)) {
            $report .= "\nRegeneration History:\n";
            foreach ($this->idHistory as $entry) {
                $report .= "  " . date('Y-m-d H:i:s', $entry['timestamp']) . 
                          " - {$entry['old_id']} → {$entry['new_id']}\n";
            }
        }
        
        return $report;
    }
}

// 使用例
echo "=== セッションID管理システム ===\n";

$manager = new SessionIdManager();

// セッション開始
session_start();
echo "\n現在のセッション情報:\n";
$current = $manager->getCurrentId();
echo "ID: {$current['session_id']}\n";
echo "長さ: {$current['length']}\n";
echo "ステータス: {$current['session_status']}\n";

// セッションID情報
echo "\n詳細情報:\n";
$info = $manager->getIdInfo();
echo "エントロピー: {$info['entropy']}\n";
echo "有効: " . ($info['valid'] ? 'Yes' : 'No') . "\n";

// セッションファイル情報
echo "\nファイル情報:\n";
$file = $manager->getSessionFilePath();
echo "パス: {$file['full_path']}\n";
echo "存在: " . ($file['exists'] ? 'Yes' : 'No') . "\n";

// ID再生成
echo "\nID再生成:\n";
$regen = $manager->regenerateId(true);
echo "旧ID: {$regen['old_id']}\n";
echo "新ID: {$regen['new_id']}\n";

// もう一度再生成
$regen2 = $manager->regenerateId(true);
echo "2回目 - 新ID: {$regen2['new_id']}\n";

// 履歴確認
echo "\n再生成履歴:\n";
foreach ($manager->getHistory() as $entry) {
    echo date('H:i:s', $entry['timestamp']) . " - ID変更\n";
}

// レポート生成
echo "\n";
echo $manager->generateReport();

// カスタムIDでの開始(新しいセッション用)
session_write_close();
echo "\n=== カスタムIDでの開始 ===\n";
$customStart = $manager->startWithCustomId();
echo "カスタムID: {$customStart['session_id']}\n";

例2: マルチデバイスセッション管理

class MultiDeviceSessionManager {
    private $storage = [];  // 実際の実装ではデータベースを使用
    
    /**
     * デバイスセッションを作成
     */
    public function createDeviceSession($userId, $deviceInfo) {
        // 新しいセッションIDを生成
        $sessionId = bin2hex(random_bytes(32));
        
        // デバイス情報を記録
        $this->storage[$sessionId] = [
            'user_id' => $userId,
            'session_id' => $sessionId,
            'device_info' => $deviceInfo,
            'created_at' => time(),
            'last_activity' => time(),
            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ];
        
        // セッションIDを設定して開始
        session_id($sessionId);
        session_start();
        
        $_SESSION['user_id'] = $userId;
        $_SESSION['device_info'] = $deviceInfo;
        $_SESSION['created_at'] = time();
        
        return [
            'session_id' => $sessionId,
            'device' => $deviceInfo['type'],
            'created' => true
        ];
    }
    
    /**
     * ユーザーのすべてのセッションを取得
     */
    public function getUserSessions($userId) {
        $sessions = [];
        
        foreach ($this->storage as $sessionId => $data) {
            if ($data['user_id'] === $userId) {
                $sessions[] = [
                    'session_id' => $sessionId,
                    'device_type' => $data['device_info']['type'] ?? 'unknown',
                    'device_name' => $data['device_info']['name'] ?? 'unknown',
                    'created_at' => $data['created_at'],
                    'last_activity' => $data['last_activity'],
                    'age' => time() - $data['created_at'],
                    'is_current' => $sessionId === session_id()
                ];
            }
        }
        
        // 最新順にソート
        usort($sessions, function($a, $b) {
            return $b['last_activity'] - $a['last_activity'];
        });
        
        return $sessions;
    }
    
    /**
     * 特定のセッションを終了
     */
    public function terminateSession($sessionId) {
        if (!isset($this->storage[$sessionId])) {
            return [
                'success' => false,
                'error' => 'Session not found'
            ];
        }
        
        $sessionData = $this->storage[$sessionId];
        
        // セッションファイルを削除
        $sessionPath = session_save_path();
        if (empty($sessionPath)) {
            $sessionPath = sys_get_temp_dir();
        }
        
        $sessionFile = $sessionPath . '/sess_' . $sessionId;
        if (file_exists($sessionFile)) {
            unlink($sessionFile);
        }
        
        // ストレージから削除
        unset($this->storage[$sessionId]);
        
        return [
            'success' => true,
            'session_id' => $sessionId,
            'device' => $sessionData['device_info']['type'] ?? 'unknown'
        ];
    }
    
    /**
     * 他のすべてのセッションを終了
     */
    public function terminateOtherSessions($userId, $currentSessionId = null) {
        if ($currentSessionId === null) {
            $currentSessionId = session_id();
        }
        
        $terminated = 0;
        $sessions = $this->getUserSessions($userId);
        
        foreach ($sessions as $session) {
            if ($session['session_id'] !== $currentSessionId) {
                $result = $this->terminateSession($session['session_id']);
                if ($result['success']) {
                    $terminated++;
                }
            }
        }
        
        return [
            'terminated' => $terminated,
            'current_session' => $currentSessionId
        ];
    }
    
    /**
     * セッションをスイッチ
     */
    public function switchSession($newSessionId) {
        if (!isset($this->storage[$newSessionId])) {
            throw new Exception("Session not found: {$newSessionId}");
        }
        
        $oldSessionId = session_id();
        
        // 現在のセッションを終了
        session_write_close();
        
        // 新しいセッションに切り替え
        session_id($newSessionId);
        session_start();
        
        // 最終アクティビティを更新
        $this->storage[$newSessionId]['last_activity'] = time();
        
        return [
            'old_session' => $oldSessionId,
            'new_session' => $newSessionId,
            'switched' => true
        ];
    }
    
    /**
     * セッション情報を更新
     */
    public function updateActivity($sessionId = null) {
        if ($sessionId === null) {
            $sessionId = session_id();
        }
        
        if (isset($this->storage[$sessionId])) {
            $this->storage[$sessionId]['last_activity'] = time();
            return true;
        }
        
        return false;
    }
    
    /**
     * 非アクティブなセッションをクリーンアップ
     */
    public function cleanupInactiveSessions($timeout = 3600) {
        $cleaned = 0;
        $cutoff = time() - $timeout;
        
        foreach ($this->storage as $sessionId => $data) {
            if ($data['last_activity'] < $cutoff) {
                $this->terminateSession($sessionId);
                $cleaned++;
            }
        }
        
        return [
            'cleaned' => $cleaned,
            'timeout' => $timeout
        ];
    }
    
    /**
     * デバイス別セッション統計
     */
    public function getSessionStatistics() {
        $stats = [
            'total_sessions' => count($this->storage),
            'by_device' => [],
            'by_user' => [],
            'active_sessions' => 0
        ];
        
        $activeThreshold = time() - 300;  // 5分以内
        
        foreach ($this->storage as $data) {
            // デバイス別
            $deviceType = $data['device_info']['type'] ?? 'unknown';
            if (!isset($stats['by_device'][$deviceType])) {
                $stats['by_device'][$deviceType] = 0;
            }
            $stats['by_device'][$deviceType]++;
            
            // ユーザー別
            $userId = $data['user_id'];
            if (!isset($stats['by_user'][$userId])) {
                $stats['by_user'][$userId] = 0;
            }
            $stats['by_user'][$userId]++;
            
            // アクティブセッション
            if ($data['last_activity'] >= $activeThreshold) {
                $stats['active_sessions']++;
            }
        }
        
        return $stats;
    }
    
    /**
     * セッション一覧を表示
     */
    public function displaySessions($userId) {
        $sessions = $this->getUserSessions($userId);
        
        echo "=== User Sessions (User ID: {$userId}) ===\n\n";
        echo "Total Sessions: " . count($sessions) . "\n\n";
        
        foreach ($sessions as $index => $session) {
            echo "Session " . ($index + 1) . ":\n";
            echo "  ID: {$session['session_id']}\n";
            echo "  Device: {$session['device_type']} - {$session['device_name']}\n";
            echo "  Created: " . date('Y-m-d H:i:s', $session['created_at']) . "\n";
            echo "  Last Activity: " . date('Y-m-d H:i:s', $session['last_activity']) . "\n";
            echo "  Age: " . round($session['age'] / 60, 2) . " minutes\n";
            echo "  Current: " . ($session['is_current'] ? 'Yes' : 'No') . "\n";
            echo "\n";
        }
    }
}

// 使用例
echo "=== マルチデバイスセッション管理 ===\n";

$multiManager = new MultiDeviceSessionManager();
$userId = 123;

// デバイス1: デスクトップ
echo "\nデスクトップセッション作成:\n";
$desktop = $multiManager->createDeviceSession($userId, [
    'type' => 'desktop',
    'name' => 'Windows PC',
    'browser' => 'Chrome'
]);
echo "セッションID: {$desktop['session_id']}\n";

// デバイス2: モバイル
session_write_close();
echo "\nモバイルセッション作成:\n";
$mobile = $multiManager->createDeviceSession($userId, [
    'type' => 'mobile',
    'name' => 'iPhone 12',
    'browser' => 'Safari'
]);
echo "セッションID: {$mobile['session_id']}\n";

// デバイス3: タブレット
session_write_close();
echo "\nタブレットセッション作成:\n";
$tablet = $multiManager->createDeviceSession($userId, [
    'type' => 'tablet',
    'name' => 'iPad Pro',
    'browser' => 'Safari'
]);
echo "セッションID: {$tablet['session_id']}\n";

// ユーザーのすべてのセッションを表示
echo "\n";
$multiManager->displaySessions($userId);

// 統計情報
echo "=== セッション統計 ===\n";
$stats = $multiManager->getSessionStatistics();
echo "総セッション数: {$stats['total_sessions']}\n";
echo "アクティブセッション: {$stats['active_sessions']}\n";
echo "\nデバイス別:\n";
foreach ($stats['by_device'] as $device => $count) {
    echo "  {$device}: {$count}\n";
}

// 特定のセッションを終了
echo "\n=== モバイルセッションを終了 ===\n";
$terminated = $multiManager->terminateSession($mobile['session_id']);
echo "終了: " . ($terminated['success'] ? 'Yes' : 'No') . "\n";

// 他のすべてのセッションを終了
session_write_close();
session_id($desktop['session_id']);
session_start();

echo "\n=== 他のすべてのセッションを終了 ===\n";
$terminateOthers = $multiManager->terminateOtherSessions($userId);
echo "終了したセッション数: {$terminateOthers['terminated']}\n";

// 残りのセッション
echo "\n残りのセッション:\n";
$multiManager->displaySessions($userId);

例3: セッションID追跡システム

class SessionIdTracker {
    private $trackingFile;
    private $tracks = [];
    
    /**
     * トラッカーを初期化
     */
    public function __construct($trackingFile = '/tmp/session_id_tracking.json') {
        $this->trackingFile = $trackingFile;
        $this->loadTracks();
    }
    
    /**
     * トラッキングデータを読み込み
     */
    private function loadTracks() {
        if (file_exists($this->trackingFile)) {
            $this->tracks = json_decode(file_get_contents($this->trackingFile), true) ?? [];
        }
    }
    
    /**
     * トラッキングデータを保存
     */
    private function saveTracks() {
        file_put_contents($this->trackingFile, json_encode($this->tracks, JSON_PRETTY_PRINT));
    }
    
    /**
     * セッションIDを追跡開始
     */
    public function startTracking($label = null) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $sessionId = session_id();
        
        if (!isset($this->tracks[$sessionId])) {
            $this->tracks[$sessionId] = [
                'session_id' => $sessionId,
                'first_seen' => time(),
                'labels' => [],
                'events' => [],
                'page_views' => 0,
                'user_agents' => [],
                'ip_addresses' => []
            ];
        }
        
        if ($label) {
            $this->tracks[$sessionId]['labels'][] = $label;
        }
        
        $this->trackEvent('tracking_started', ['label' => $label]);
        
        return $this->tracks[$sessionId];
    }
    
    /**
     * イベントを記録
     */
    public function trackEvent($eventType, $data = []) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $sessionId = session_id();
        
        if (!isset($this->tracks[$sessionId])) {
            $this->startTracking();
        }
        
        $event = [
            'type' => $eventType,
            'timestamp' => time(),
            'data' => $data,
            'url' => $_SERVER['REQUEST_URI'] ?? 'unknown',
            'referer' => $_SERVER['HTTP_REFERER'] ?? null
        ];
        
        $this->tracks[$sessionId]['events'][] = $event;
        $this->tracks[$sessionId]['last_activity'] = time();
        
        // ページビューカウント
        if ($eventType === 'page_view') {
            $this->tracks[$sessionId]['page_views']++;
        }
        
        // User Agent記録
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
        if (!in_array($userAgent, $this->tracks[$sessionId]['user_agents'])) {
            $this->tracks[$sessionId]['user_agents'][] = $userAgent;
        }
        
        // IPアドレス記録
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        if (!in_array($ip, $this->tracks[$sessionId]['ip_addresses'])) {
            $this->tracks[$sessionId]['ip_addresses'][] = $ip;
        }
        
        $this->saveTracks();
        
        return $event;
    }
    
    /**
     * ページビューを記録
     */
    public function trackPageView($page = null) {
        $page = $page ?? ($_SERVER['REQUEST_URI'] ?? 'unknown');
        
        return $this->trackEvent('page_view', [
            'page' => $page,
            'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown'
        ]);
    }
    
    /**
     * ユーザーアクションを記録
     */
    public function trackAction($action, $details = []) {
        return $this->trackEvent('user_action', [
            'action' => $action,
            'details' => $details
        ]);
    }
    
    /**
     * セッション情報を取得
     */
    public function getSessionInfo($sessionId = null) {
        if ($sessionId === null) {
            if (session_status() !== PHP_SESSION_ACTIVE) {
                session_start();
            }
            $sessionId = session_id();
        }
        
        if (!isset($this->tracks[$sessionId])) {
            return null;
        }
        
        $track = $this->tracks[$sessionId];
        $duration = isset($track['last_activity']) ? 
            $track['last_activity'] - $track['first_seen'] : 0;
        
        return [
            'session_id' => $sessionId,
            'first_seen' => $track['first_seen'],
            'last_activity' => $track['last_activity'] ?? $track['first_seen'],
            'duration' => $duration,
            'page_views' => $track['page_views'],
            'total_events' => count($track['events']),
            'labels' => $track['labels'],
            'unique_ips' => count($track['ip_addresses']),
            'unique_user_agents' => count($track['user_agents'])
        ];
    }
    
    /**
     * セッションのタイムラインを取得
     */
    public function getTimeline($sessionId = null) {
        if ($sessionId === null) {
            if (session_status() !== PHP_SESSION_ACTIVE) {
                session_start();
            }
            $sessionId = session_id();
        }
        
        if (!isset($this->tracks[$sessionId])) {
            return [];
        }
        
        return $this->tracks[$sessionId]['events'];
    }
    
    /**
     * すべてのセッションを取得
     */
    public function getAllSessions($limit = null) {
        $sessions = [];
        
        foreach ($this->tracks as $sessionId => $track) {
            $sessions[] = $this->getSessionInfo($sessionId);
        }
        
        // 最新順にソート
        usort($sessions, function($a, $b) {
            return $b['last_activity'] - $a['last_activity'];
        });
        
        if ($limit !== null) {
            $sessions = array_slice($sessions, 0, $limit);
        }
        
        return $sessions;
    }
    
    /**
     * 統計情報を取得
     */
    public function getStatistics() {
        $totalSessions = count($this->tracks);
        $totalPageViews = 0;
        $totalEvents = 0;
        $activeThreshold = time() - 1800;  // 30分
        $activeSessions = 0;
        
        foreach ($this->tracks as $track) {
            $totalPageViews += $track['page_views'];
            $totalEvents += count($track['events']);
            
            if (isset($track['last_activity']) && $track['last_activity'] >= $activeThreshold) {
                $activeSessions++;
            }
        }
        
        return [
            'total_sessions' => $totalSessions,
            'active_sessions' => $activeSessions,
            'total_page_views' => $totalPageViews,
            'total_events' => $totalEvents,
            'average_page_views' => $totalSessions > 0 ? 
                round($totalPageViews / $totalSessions, 2) : 0,
            'average_events' => $totalSessions > 0 ? 
                round($totalEvents / $totalSessions, 2) : 0
        ];
    }
    
    /**
     * レポートを生成
     */
    public function generateReport($sessionId = null) {
        if ($sessionId === null) {
            // 全体レポート
            $stats = $this->getStatistics();
            
            $report = "=== Session Tracking Report ===\n\n";
            $report .= "Total Sessions: {$stats['total_sessions']}\n";
            $report .= "Active Sessions: {$stats['active_sessions']}\n";
            $report .= "Total Page Views: {$stats['total_page_views']}\n";
            $report .= "Total Events: {$stats['total_events']}\n";
            $report .= "Average Page Views: {$stats['average_page_views']}\n";
            $report .= "Average Events: {$stats['average_events']}\n\n";
            
            $report .= "Recent Sessions:\n";
            foreach ($this->getAllSessions(5) as $session) {
                $report .= "  {$session['session_id']}: ";
                $report .= "{$session['page_views']} views, ";
                $report .= date('Y-m-d H:i:s', $session['last_activity']) . "\n";
            }
        } else {
            // 個別セッションレポート
            $info = $this->getSessionInfo($sessionId);
            
            if ($info === null) {
                return "Session not found: {$sessionId}";
            }
            
            $report = "=== Session Report ===\n\n";
            $report .= "Session ID: {$info['session_id']}\n";
            $report .= "First Seen: " . date('Y-m-d H:i:s', $info['first_seen']) . "\n";
            $report .= "Last Activity: " . date('Y-m-d H:i:s', $info['last_activity']) . "\n";
            $report .= "Duration: " . round($info['duration'] / 60, 2) . " minutes\n";
            $report .= "Page Views: {$info['page_views']}\n";
            $report .= "Total Events: {$info['total_events']}\n";
            $report .= "Unique IPs: {$info['unique_ips']}\n";
            
            if (!empty($info['labels'])) {
                $report .= "Labels: " . implode(', ', $info['labels']) . "\n";
            }
            
            $report .= "\nTimeline:\n";
            foreach ($this->getTimeline($sessionId) as $event) {
                $report .= "  [" . date('H:i:s', $event['timestamp']) . "] ";
                $report .= "{$event['type']}\n";
            }
        }
        
        return $report;
    }
}

// 使用例
echo "=== セッションID追跡システム ===\n";

$tracker = new SessionIdTracker('/tmp/session_tracking_test.json');

// 追跡開始
session_start();
$tracker->startTracking('test_session');
echo "追跡開始: " . session_id() . "\n";

// ページビューを記録
$tracker->trackPageView('/home');
$tracker->trackPageView('/products');
$tracker->trackPageView('/checkout');

// ユーザーアクションを記録
$tracker->trackAction('add_to_cart', ['product_id' => 123, 'quantity' => 2]);
$tracker->trackAction('checkout_started');
$tracker->trackAction('payment_completed', ['amount' => 5000]);

// セッション情報
echo "\nセッション情報:\n";
$info = $tracker->getSessionInfo();
echo "ページビュー数: {$info['page_views']}\n";
echo "総イベント数: {$info['total_events']}\n";
echo "セッション時間: " . round($info['duration'] / 60, 2) . "分\n";

// タイムライン
echo "\nタイムライン:\n";
foreach ($tracker->getTimeline() as $index => $event) {
    echo ($index + 1) . ". [" . date('H:i:s', $event['timestamp']) . "] {$event['type']}\n";
}

// 統計情報
echo "\n統計情報:\n";
$stats = $tracker->getStatistics();
echo "総セッション数: {$stats['total_sessions']}\n";
echo "総ページビュー数: {$stats['total_page_views']}\n";
echo "平均ページビュー: {$stats['average_page_views']}\n";

// レポート生成
echo "\n";
echo $tracker->generateReport(session_id());

例4: セッションID検証システム

class SessionIdValidator {
    private $validationRules = [];
    private $validationHistory = [];
    
    /**
     * バリデーターを初期化
     */
    public function __construct() {
        $this->setupDefaultRules();
    }
    
    /**
     * デフォルトのバリデーションルールを設定
     */
    private function setupDefaultRules() {
        // 長さチェック
        $this->addRule('length', function($id) {
            $length = strlen($id);
            return [
                'valid' => $length >= 22 && $length <= 128,
                'message' => $length >= 22 && $length <= 128 ? 
                    'Length is valid' : 
                    "Length {$length} is out of range (22-128)",
                'severity' => $length >= 22 && $length <= 128 ? 'none' : 'error'
            ];
        });
        
        // 文字セットチェック
        $this->addRule('charset', function($id) {
            $valid = preg_match('/^[a-zA-Z0-9,-]+$/', $id);
            return [
                'valid' => $valid,
                'message' => $valid ? 
                    'Character set is valid' : 
                    'Contains invalid characters',
                'severity' => $valid ? 'none' : 'error'
            ];
        });
        
        // エントロピーチェック
        $this->addRule('entropy', function($id) {
            $entropy = $this->calculateEntropy($id);
            $valid = $entropy >= 3.5;  // 十分なランダム性
            return [
                'valid' => $valid,
                'message' => $valid ? 
                    "Entropy is good ({$entropy})" : 
                    "Low entropy ({$entropy})",
                'severity' => $valid ? 'none' : 'warning',
                'value' => $entropy
            ];
        });
        
        // パターンチェック(予測可能なパターンを検出)
        $this->addRule('pattern', function($id) {
            // 連続した同じ文字のチェック
            if (preg_match('/(.)\1{5,}/', $id)) {
                return [
                    'valid' => false,
                    'message' => 'Contains repeated character patterns',
                    'severity' => 'warning'
                ];
            }
            
            // 連続した数字のチェック
            if (preg_match('/0123|1234|2345|3456|4567|5678|6789/', $id)) {
                return [
                    'valid' => false,
                    'message' => 'Contains sequential patterns',
                    'severity' => 'warning'
                ];
            }
            
            return [
                'valid' => true,
                'message' => 'No suspicious patterns detected',
                'severity' => 'none'
            ];
        });
        
        // 一意性チェック(既存のセッションIDと重複していないか)
        $this->addRule('uniqueness', function($id) {
            $sessionPath = session_save_path();
            if (empty($sessionPath)) {
                $sessionPath = sys_get_temp_dir();
            }
            
            $sessionFile = $sessionPath . '/sess_' . $id;
            $exists = file_exists($sessionFile);
            
            return [
                'valid' => true,  // 存在しても必ずしもエラーではない
                'message' => $exists ? 
                    'Session file already exists' : 
                    'Session ID is unique',
                'severity' => $exists ? 'info' : 'none',
                'exists' => $exists
            ];
        });
    }
    
    /**
     * エントロピーを計算
     */
    private function calculateEntropy($string) {
        $chars = str_split($string);
        $frequency = array_count_values($chars);
        $length = strlen($string);
        
        $entropy = 0;
        foreach ($frequency as $count) {
            $p = $count / $length;
            $entropy -= $p * log($p, 2);
        }
        
        return round($entropy, 2);
    }
    
    /**
     * バリデーションルールを追加
     */
    public function addRule($name, $callback) {
        $this->validationRules[$name] = $callback;
    }
    
    /**
     * セッションIDを検証
     */
    public function validate($sessionId = null) {
        if ($sessionId === null) {
            if (session_status() !== PHP_SESSION_ACTIVE) {
                session_start();
            }
            $sessionId = session_id();
        }
        
        $results = [
            'session_id' => $sessionId,
            'timestamp' => time(),
            'rules' => [],
            'valid' => true,
            'errors' => 0,
            'warnings' => 0
        ];
        
        foreach ($this->validationRules as $ruleName => $callback) {
            $result = call_user_func($callback, $sessionId);
            $results['rules'][$ruleName] = $result;
            
            if (!$result['valid']) {
                $results['valid'] = false;
            }
            
            if ($result['severity'] === 'error') {
                $results['errors']++;
            } elseif ($result['severity'] === 'warning') {
                $results['warnings']++;
            }
        }
        
        // 履歴に記録
        $this->validationHistory[] = $results;
        
        return $results;
    }
    
    /**
     * セキュリティスコアを計算
     */
    public function calculateSecurityScore($sessionId = null) {
        $validation = $this->validate($sessionId);
        
        $score = 100;
        
        foreach ($validation['rules'] as $rule) {
            if ($rule['severity'] === 'error') {
                $score -= 25;
            } elseif ($rule['severity'] === 'warning') {
                $score -= 10;
            }
        }
        
        $score = max(0, $score);
        
        return [
            'score' => $score,
            'grade' => $this->getGrade($score),
            'validation' => $validation
        ];
    }
    
    /**
     * グレードを取得
     */
    private function getGrade($score) {
        if ($score >= 90) return 'A';
        if ($score >= 80) return 'B';
        if ($score >= 70) return 'C';
        if ($score >= 60) return 'D';
        return 'F';
    }
    
    /**
     * バリデーションレポートを生成
     */
    public function generateReport($sessionId = null) {
        $validation = $this->validate($sessionId);
        
        $report = "=== Session ID Validation Report ===\n\n";
        $report .= "Session ID: {$validation['session_id']}\n";
        $report .= "Status: " . ($validation['valid'] ? 'VALID' : 'INVALID') . "\n";
        $report .= "Errors: {$validation['errors']}\n";
        $report .= "Warnings: {$validation['warnings']}\n\n";
        
        $report .= "Validation Results:\n";
        foreach ($validation['rules'] as $ruleName => $result) {
            $status = $result['valid'] ? '✓' : '✗';
            $report .= "  {$status} {$ruleName}: {$result['message']}";
            if ($result['severity'] !== 'none') {
                $report .= " [{$result['severity']}]";
            }
            $report .= "\n";
        }
        
        // セキュリティスコア
        $score = $this->calculateSecurityScore($validation['session_id']);
        $report .= "\nSecurity Score: {$score['score']}/100 (Grade: {$score['grade']})\n";
        
        return $report;
    }
    
    /**
     * 複数のセッションIDを一括検証
     */
    public function batchValidate($sessionIds) {
        $results = [];
        
        foreach ($sessionIds as $id) {
            $results[$id] = $this->validate($id);
        }
        
        return [
            'total' => count($sessionIds),
            'valid' => count(array_filter($results, function($r) { return $r['valid']; })),
            'invalid' => count(array_filter($results, function($r) { return !$r['valid']; })),
            'results' => $results
        ];
    }
    
    /**
     * セキュアなセッションIDを生成
     */
    public function generateSecureId($length = 32) {
        // 暗号的に安全な乱数を生成
        $bytes = random_bytes($length);
        $id = bin2hex($bytes);
        
        // 生成したIDを検証
        $validation = $this->validate($id);
        
        return [
            'session_id' => $id,
            'validation' => $validation,
            'secure' => $validation['valid']
        ];
    }
    
    /**
     * 検証履歴を取得
     */
    public function getValidationHistory($limit = null) {
        $history = $this->validationHistory;
        
        if ($limit !== null) {
            $history = array_slice($history, -$limit);
        }
        
        return $history;
    }
}

// 使用例
echo "=== セッションID検証システム ===\n";

$validator = new SessionIdValidator();

// 現在のセッションIDを検証
session_start();
echo "\n現在のセッションIDを検証:\n";
echo $validator->generateReport();

// セキュリティスコア
$score = $validator->calculateSecurityScore();
echo "\nセキュリティスコア: {$score['score']}/100 (グレード: {$score['grade']})\n";

// カスタムIDの検証
echo "\n=== カスタムIDの検証 ===\n";

// 良いID
$goodId = bin2hex(random_bytes(32));
echo "良いID: {$goodId}\n";
$goodValidation = $validator->validate($goodId);
echo "有効: " . ($goodValidation['valid'] ? 'Yes' : 'No') . "\n";
echo "エラー: {$goodValidation['errors']}, 警告: {$goodValidation['warnings']}\n";

// 悪いID(短すぎる)
echo "\n短すぎるID:\n";
$badId1 = 'short';
$badValidation1 = $validator->validate($badId1);
echo "ID: {$badId1}\n";
echo "有効: " . ($badValidation1['valid'] ? 'Yes' : 'No') . "\n";
echo "エラー: {$badValidation1['errors']}\n";

// 悪いID(無効な文字)
echo "\n無効な文字を含むID:\n";
$badId2 = 'invalid@characters#here!';
$badValidation2 = $validator->validate($badId2);
echo "ID: {$badId2}\n";
echo "有効: " . ($badValidation2['valid'] ? 'Yes' : 'No') . "\n";

// 一括検証
echo "\n=== 一括検証 ===\n";
$idsToValidate = [
    bin2hex(random_bytes(32)),
    'short',
    bin2hex(random_bytes(16)),
    'invalid@id'
];

$batchResults = $validator->batchValidate($idsToValidate);
echo "総数: {$batchResults['total']}\n";
echo "有効: {$batchResults['valid']}\n";
echo "無効: {$batchResults['invalid']}\n";

// セキュアなIDを生成
echo "\n=== セキュアなID生成 ===\n";
$generated = $validator->generateSecureId();
echo "生成されたID: {$generated['session_id']}\n";
echo "セキュア: " . ($generated['secure'] ? 'Yes' : 'No') . "\n";

例5: セッション固定攻撃対策システム

class SessionFixationProtection {
    private $regenerationLog = [];
    
    /**
     * セッション固定攻撃を防ぐ
     */
    public function protectAgainstFixation() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        // セッションが初回アクセスか確認
        if (!isset($_SESSION['initialized'])) {
            // 初回アクセス時は必ずIDを再生成
            $this->regenerateWithLog('initial_access');
            $_SESSION['initialized'] = true;
            $_SESSION['created_at'] = time();
            
            return [
                'action' => 'initialized',
                'reason' => 'First access - regenerated ID'
            ];
        }
        
        // 認証状態の変化をチェック
        if (isset($_SESSION['authentication_changed']) && $_SESSION['authentication_changed']) {
            $this->regenerateWithLog('authentication_changed');
            unset($_SESSION['authentication_changed']);
            
            return [
                'action' => 'regenerated',
                'reason' => 'Authentication state changed'
            ];
        }
        
        // 定期的な再生成(例:30分ごと)
        $lastRegeneration = $_SESSION['last_regenerated'] ?? $_SESSION['created_at'];
        $interval = 1800;  // 30分
        
        if (time() - $lastRegeneration > $interval) {
            $this->regenerateWithLog('periodic');
            
            return [
                'action' => 'regenerated',
                'reason' => 'Periodic regeneration'
            ];
        }
        
        return [
            'action' => 'none',
            'reason' => 'No regeneration needed'
        ];
    }
    
    /**
     * ログ付きでIDを再生成
     */
    private function regenerateWithLog($reason) {
        $oldId = session_id();
        
        session_regenerate_id(true);
        
        $newId = session_id();
        
        $this->regenerationLog[] = [
            'timestamp' => time(),
            'old_id' => $oldId,
            'new_id' => $newId,
            'reason' => $reason
        ];
        
        $_SESSION['last_regenerated'] = time();
        $_SESSION['regeneration_count'] = ($_SESSION['regeneration_count'] ?? 0) + 1;
    }
    
    /**
     * ログイン時の処理
     */
    public function onLogin($userId) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        // 古いセッションを破棄して新しいIDを生成
        $oldId = session_id();
        session_regenerate_id(true);
        $newId = session_id();
        
        // セッションデータを設定
        $_SESSION['user_id'] = $userId;
        $_SESSION['authenticated'] = true;
        $_SESSION['login_time'] = time();
        $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
        
        $this->regenerationLog[] = [
            'timestamp' => time(),
            'old_id' => $oldId,
            'new_id' => $newId,
            'reason' => 'login',
            'user_id' => $userId
        ];
        
        return [
            'old_session_id' => $oldId,
            'new_session_id' => $newId,
            'user_id' => $userId
        ];
    }
    
    /**
     * 権限昇格時の処理
     */
    public function onPrivilegeEscalation($newRole) {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $oldRole = $_SESSION['role'] ?? 'guest';
        
        // 権限が上がる場合は必ずIDを再生成
        $this->regenerateWithLog('privilege_escalation');
        
        $_SESSION['role'] = $newRole;
        $_SESSION['role_changed_at'] = time();
        
        return [
            'old_role' => $oldRole,
            'new_role' => $newRole,
            'session_id' => session_id()
        ];
    }
    
    /**
     * セッションハイジャックを検出
     */
    public function detectHijacking() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $issues = [];
        
        // IPアドレスの変化をチェック
        $currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        if (isset($_SESSION['ip_address']) && $_SESSION['ip_address'] !== $currentIp) {
            $issues[] = [
                'type' => 'ip_mismatch',
                'severity' => 'high',
                'stored_ip' => $_SESSION['ip_address'],
                'current_ip' => $currentIp
            ];
        }
        
        // User Agentの変化をチェック
        $currentUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
        if (isset($_SESSION['user_agent']) && $_SESSION['user_agent'] !== $currentUserAgent) {
            $issues[] = [
                'type' => 'user_agent_mismatch',
                'severity' => 'medium',
                'stored_ua' => $_SESSION['user_agent'],
                'current_ua' => $currentUserAgent
            ];
        }
        
        // セッションの年齢チェック
        if (isset($_SESSION['created_at'])) {
            $age = time() - $_SESSION['created_at'];
            $maxAge = 86400;  // 24時間
            
            if ($age > $maxAge) {
                $issues[] = [
                    'type' => 'session_too_old',
                    'severity' => 'medium',
                    'age' => $age,
                    'max_age' => $maxAge
                ];
            }
        }
        
        return [
            'hijacking_detected' => !empty($issues),
            'issues' => $issues,
            'issue_count' => count($issues)
        ];
    }
    
    /**
     * セッションを無効化
     */
    public function invalidateSession($reason = 'security') {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $sessionId = session_id();
        
        $_SESSION = [];
        
        if (isset($_COOKIE[session_name()])) {
            setcookie(session_name(), '', time() - 3600, '/');
        }
        
        session_destroy();
        
        $this->regenerationLog[] = [
            'timestamp' => time(),
            'session_id' => $sessionId,
            'action' => 'invalidated',
            'reason' => $reason
        ];
        
        return [
            'invalidated' => true,
            'session_id' => $sessionId,
            'reason' => $reason
        ];
    }
    
    /**
     * 再生成ログを取得
     */
    public function getRegenerationLog() {
        return $this->regenerationLog;
    }
    
    /**
     * セキュリティレポートを生成
     */
    public function generateSecurityReport() {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
        
        $hijackingCheck = $this->detectHijacking();
        
        $report = "=== Session Security Report ===\n\n";
        $report .= "Current Session ID: " . session_id() . "\n";
        $report .= "Created: " . (isset($_SESSION['created_at']) ? 
            date('Y-m-d H:i:s', $_SESSION['created_at']) : 'Unknown') . "\n";
        $report .= "Authenticated: " . (isset($_SESSION['authenticated']) ? 'Yes' : 'No') . "\n";
        
        if (isset($_SESSION['regeneration_count'])) {
            $report .= "Regenerations: {$_SESSION['regeneration_count']}\n";
        }
        
        $report .= "\nHijacking Detection:\n";
        $report .= "Status: " . ($hijackingCheck['hijacking_detected'] ? 'SUSPICIOUS' : 'OK') . "\n";
        
        if ($hijackingCheck['hijacking_detected']) {
            $report .= "Issues Found:\n";
            foreach ($hijackingCheck['issues'] as $issue) {
                $report .= "  [{$issue['severity']}] {$issue['type']}\n";
            }
        }
        
        if (!empty($this->regenerationLog)) {
            $report .= "\nRegeneration History:\n";
            foreach (array_slice($this->regenerationLog, -5) as $entry) {
                $report .= "  " . date('Y-m-d H:i:s', $entry['timestamp']) . 
                          " - {$entry['reason']}\n";
            }
        }
        
        return $report;
    }
}

// 使用例
echo "=== セッション固定攻撃対策 ===\n";

$protection = new SessionFixationProtection();

// セッション開始と保護
session_start();
echo "\n初回アクセス保護:\n";
$result1 = $protection->protectAgainstFixation();
echo "アクション: {$result1['action']}\n";
echo "理由: {$result1['reason']}\n";

// ログインシミュレーション
echo "\n=== ログイン処理 ===\n";
$login = $protection->onLogin(123);
echo "旧ID: {$login['old_session_id']}\n";
echo "新ID: {$login['new_session_id']}\n";
echo "ユーザーID: {$login['user_id']}\n";

// 権限昇格
echo "\n=== 権限昇格 ===\n";
$escalation = $protection->onPrivilegeEscalation('admin');
echo "旧ロール: {$escalation['old_role']}\n";
echo "新ロール: {$escalation['new_role']}\n";
echo "セッションID: {$escalation['session_id']}\n";

// ハイジャック検出
echo "\n=== ハイジャック検出 ===\n";
$hijacking = $protection->detectHijacking();
echo "検出: " . ($hijacking['hijacking_detected'] ? 'Yes' : 'No') . "\n";
echo "問題数: {$hijacking['issue_count']}\n";

// セキュリティレポート
echo "\n";
echo $protection->generateSecurityReport();

// 再生成ログ
echo "\n=== 再生成ログ ===\n";
foreach ($protection->getRegenerationLog() as $entry) {
    echo date('H:i:s', $entry['timestamp']) . " - {$entry['reason']}\n";
}

例6: セッションID暗号化システム

class SessionIdEncryptor {
    private $encryptionKey;
    private $cipher = 'AES-256-CBC';
    
    /**
     * 暗号化システムを初期化
     */
    public function __construct($encryptionKey = null) {
        if ($encryptionKey === null) {
            // 暗号化キーを生成
            $this->encryptionKey = random_bytes(32);
        } else {
            $this->encryptionKey = $encryptionKey;
        }
    }
    
    /**
     * セッションIDを暗号化
     */
    public function encrypt($sessionId) {
        $ivLength = openssl_cipher_iv_length($this->cipher);
        $iv = openssl_random_pseudo_bytes($ivLength);
        
        $encrypted = openssl_encrypt(
            $sessionId,
            $this->cipher,
            $this->encryptionKey,
            0,
            $iv
        );
        
        // IVと暗号化データを結合
        $result = base64_encode($iv . $encrypted);
        
        return [
            'encrypted' => $result,
            'original_length' => strlen($sessionId),
            'encrypted_length' => strlen($result),
            'cipher' => $this->cipher
        ];
    }
    
    /**
     * 暗号化されたセッションIDを復号化
     */
    public function decrypt($encryptedData) {
        $data = base64_decode($encryptedData);
        
        $ivLength = openssl_cipher_iv_length($this->cipher);
        $iv = substr($data, 0, $ivLength);
        $encrypted = substr($data, $ivLength);
        
        $decrypted = openssl_decrypt(
            $encrypted,
            $this->cipher,
            $this->encryptionKey,
            0,
            $iv
        );
        
        return [
            'decrypted' => $decrypted,
            'success' => $decrypted !== false,
            'length' => strlen($decrypted)
        ];
    }
    
    /**
     * セッションIDにハッシュ署名を追加
     */
    public function sign($sessionId) {
        $signature = hash_hmac('sha256', $sessionId, $this->encryptionKey);
        
        return [
            'session_id' => $sessionId,
            'signature' => $signature,
            'signed' => $sessionId . '.' . $signature
        ];
    }
    
    /**
     * 署名を検証
     */
    public function verify($signedData) {
        $parts = explode('.', $signedData);
        
        if (count($parts) !== 2) {
            return [
                'valid' => false,
                'error' => 'Invalid format'
            ];
        }
        
        list($sessionId, $signature) = $parts;
        
        $expectedSignature = hash_hmac('sha256', $sessionId, $this->encryptionKey);
        $valid = hash_equals($expectedSignature, $signature);
        
        return [
            'valid' => $valid,
            'session_id' => $valid ? $sessionId : null,
            'signature' => $signature
        ];
    }
    
    /**
     * セキュアトークンを生成
     */
    public function generateSecureToken($sessionId, $expiresIn = 3600) {
        $payload = [
            'session_id' => $sessionId,
            'created_at' => time(),
            'expires_at' => time() + $expiresIn
        ];
        
        $json = json_encode($payload);
        $encrypted = $this->encrypt($json);
        
        return [
            'token' => $encrypted['encrypted'],
            'expires_at' => $payload['expires_at'],
            'valid_for' => $expiresIn
        ];
    }
    
    /**
     * トークンを検証
     */
    public function validateToken($token) {
        $decrypted = $this->decrypt($token);
        
        if (!$decrypted['success']) {
            return [
                'valid' => false,
                'error' => 'Decryption failed'
            ];
        }
        
        $payload = json_decode($decrypted['decrypted'], true);
        
        if ($payload === null) {
            return [
                'valid' => false,
                'error' => 'Invalid payload'
            ];
        }
        
        // 有効期限チェック
        if ($payload['expires_at'] < time()) {
            return [
                'valid' => false,
                'error' => 'Token expired',
                'expired_at' => $payload['expires_at']
            ];
        }
        
        return [
            'valid' => true,
            'session_id' => $payload['session_id'],
            'created_at' => $payload['created_at'],
            'expires_at' => $payload['expires_at'],
            'remaining' => $payload['expires_at'] - time()
        ];
    }
    
    /**
     * 暗号化キーをエクスポート
     */
    public function exportKey() {
        return base64_encode($this->encryptionKey);
    }
    
    /**
     * 暗号化キーをインポート
     */
    public function importKey($encodedKey) {
        $this->encryptionKey = base64_decode($encodedKey);
    }
}

// 使用例
echo "=== セッションID暗号化システム ===\n";

$encryptor = new SessionIdEncryptor();

// セッションIDを取得
session_start();
$sessionId = session_id();
echo "元のセッションID: {$sessionId}\n";
echo "長さ: " . strlen($sessionId) . " 文字\n";

// 暗号化
echo "\n=== 暗号化 ===\n";
$encrypted = $encryptor->encrypt($sessionId);
echo "暗号化データ: {$encrypted['encrypted']}\n";
echo "元の長さ: {$encrypted['original_length']}\n";
echo "暗号化後の長さ: {$encrypted['encrypted_length']}\n";

// 復号化
echo "\n=== 復号化 ===\n";
$decrypted = $encryptor->decrypt($encrypted['encrypted']);
echo "復号化成功: " . ($decrypted['success'] ? 'Yes' : 'No') . "\n";
echo "復号化データ: {$decrypted['decrypted']}\n";
echo "一致: " . ($decrypted['decrypted'] === $sessionId ? 'Yes' : 'No') . "\n";

// 署名
echo "\n=== 署名 ===\n";
$signed = $encryptor->sign($sessionId);
echo "署名付きデータ: {$signed['signed']}\n";
echo "署名: " . substr($signed['signature'], 0, 16) . "...\n";

// 署名検証
echo "\n=== 署名検証 ===\n";
$verified = $encryptor->verify($signed['signed']);
echo "有効: " . ($verified['valid'] ? 'Yes' : 'No') . "\n";
echo "セッションID: {$verified['session_id']}\n";

// 改ざんされたデータの検証
echo "\n改ざんされたデータ:\n";
$tampered = $sessionId . '.invalid_signature';
$tamperedVerify = $encryptor->verify($tampered);
echo "有効: " . ($tamperedVerify['valid'] ? 'Yes' : 'No') . "\n";

// セキュアトークン生成
echo "\n=== セキュアトークン ===\n";
$token = $encryptor->generateSecureToken($sessionId, 1800);  // 30分
echo "トークン: " . substr($token['token'], 0, 50) . "...\n";
echo "有効期限: " . date('Y-m-d H:i:s', $token['expires_at']) . "\n";

// トークン検証
echo "\nトークン検証:\n";
$tokenValidation = $encryptor->validateToken($token['token']);
echo "有効: " . ($tokenValidation['valid'] ? 'Yes' : 'No') . "\n";
echo "残り時間: " . round($tokenValidation['remaining'] / 60, 2) . "分\n";

// 暗号化キーのエクスポート/インポート
echo "\n=== キー管理 ===\n";
$exportedKey = $encryptor->exportKey();
echo "エクスポートされたキー: " . substr($exportedKey, 0, 20) . "...\n";

セキュリティのベストプラクティス

// セッションID管理のセキュリティ対策

// 1. セッション開始時の初期化
session_start();
if (!isset($_SESSION['initialized'])) {
    session_regenerate_id(true);
    $_SESSION['initialized'] = true;
    $_SESSION['created_at'] = time();
}

// 2. ログイン時の再生成
function secureLogin($userId) {
    session_start();
    
    // 古いセッションを破棄
    session_regenerate_id(true);
    
    // 新しいセッションに情報を設定
    $_SESSION['user_id'] = $userId;
    $_SESSION['login_time'] = time();
    $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}

// 3. 定期的な再生成
session_start();
$lastRegen = $_SESSION['last_regenerated'] ?? time();
if (time() - $lastRegen > 1800) {  // 30分ごと
    session_regenerate_id(true);
    $_SESSION['last_regenerated'] = time();
}

// 4. セッションハイジャック検出
function detectHijacking() {
    session_start();
    
    $currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
    $storedIp = $_SESSION['ip'] ?? $currentIp;
    
    if ($currentIp !== $storedIp) {
        // IPが変わった場合は無効化
        session_destroy();
        return true;
    }
    
    return false;
}

// 5. カスタムIDの検証
function setCustomSessionId($id) {
    // IDの検証
    if (!preg_match('/^[a-zA-Z0-9,-]{32,128}$/', $id)) {
        throw new Exception('Invalid session ID format');
    }
    
    // セッション開始前に設定
    if (session_status() === PHP_SESSION_ACTIVE) {
        throw new Exception('Session already started');
    }
    
    session_id($id);
    session_start();
}

セッションIDの取得パターン

// 様々な状況でのセッションID取得

// 1. 基本的な取得
session_start();
$id = session_id();

// 2. セッション開始前の確認
if (session_status() === PHP_SESSION_ACTIVE) {
    $id = session_id();
} else {
    session_start();
    $id = session_id();
}

// 3. クッキーからの取得
$sessionName = session_name();
$cookieId = $_COOKIE[$sessionName] ?? null;

// 4. セッション開始前の取得(空文字列が返る)
$id = session_id();  // セッション未開始なら空文字列

// 5. 設定後の確認
$customId = bin2hex(random_bytes(16));
session_id($customId);
session_start();
echo session_id();  // $customIdと同じ

まとめ

session_id()関数の特徴をまとめると:

できること:

  • 現在のセッションIDの取得
  • セッション開始前のID設定
  • セッションIDの検証
  • カスタムセッションIDの使用

基本的な使い方:

// 取得
session_start();
$id = session_id();

// 設定(session_start()前)
session_id('custom_id');
session_start();

重要な注意点:

  • セッション開始後は設定不可
  • 英数字、カンマ、ハイフンのみ使用可能
  • 長さは22〜128文字
  • PHPが自動生成するIDは暗号的に安全

セキュリティ対策:

// 1. ログイン時は必ず再生成
session_regenerate_id(true);

// 2. 定期的な再生成
if (time() - $_SESSION['last_regen'] > 1800) {
    session_regenerate_id(true);
    $_SESSION['last_regen'] = time();
}

// 3. セッション固定攻撃対策
// 初回アクセス時にIDを再生成
if (!isset($_SESSION['initialized'])) {
    session_regenerate_id(true);
    $_SESSION['initialized'] = true;
}

// 4. ハイジャック検出
if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
    session_destroy();
}

推奨される使用場面:

  • セッション固定攻撃対策
  • マルチデバイス管理
  • セッション追跡・監査
  • カスタムセッション実装
  • セキュリティ検証

ベストプラクティス:

// 1. セキュアな初期化
session_start();
if (!isset($_SESSION['created'])) {
    session_regenerate_id(true);
    $_SESSION['created'] = time();
    $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
}

// 2. ログイン処理
function login($userId) {
    session_start();
    session_regenerate_id(true);  // 必須
    $_SESSION['user_id'] = $userId;
    $_SESSION['login_time'] = time();
}

// 3. 権限変更時
function elevatePrivileges($newRole) {
    session_start();
    session_regenerate_id(true);  // 必須
    $_SESSION['role'] = $newRole;
}

// 4. 定期的なメンテナンス
if (time() - ($_SESSION['last_check'] ?? 0) > 300) {
    // 5分ごとにチェック
    if (time() - $_SESSION['created'] > 1800) {
        session_regenerate_id(true);
        $_SESSION['created'] = time();
    }
    $_SESSION['last_check'] = time();
}

関連関数:

  • session_regenerate_id(): セッションID再生成(推奨)
  • session_start(): セッション開始
  • session_name(): セッション名取得/設定
  • session_destroy(): セッション破棄
  • session_status(): セッション状態確認

session_regenerate_id()との使い分け:

// session_regenerate_id() - 推奨
session_start();
session_regenerate_id(true);  // セッション継続中に安全に再生成

// session_id() - 特殊な用途のみ
session_id('custom_id');  // セッション開始前にカスタムID設定
session_start();

session_id()は、セッションIDの取得と設定を行う基本的な関数です。セキュリティを考慮した適切な使用で、安全なセッション管理を実現しましょう!

タイトルとURLをコピーしました