Skip to content

Session 安全

Session 安全是 Web 应用安全的重要组成部分。Session 劫持、Session 固定和 Session 预测是常见的攻击方式。本节讲解 PHP Session 的安全配置、常见攻击的防御策略,以及安全 Session 管理的最佳实践。

前置知识

阅读本节前,建议先了解:Session 基础安全总则

基础概念

Session 攻击类型

攻击类型原理防御方式
Session Fixation攻击者预设 Session IDsession_regenerate_id()
Session Hijacking窃取有效 Session IDHTTPS + HttpOnly + 指纹
Session Prediction猜测 Session ID高熵值 Session ID
Session Replay重放已使用的 Session一次性 Token + 时间窗口

安全核心

Session ID 是用户身份的唯一标识。保护 Session ID 的生成、传输和存储安全是 Session 安全的核心。

Session 安全配置

php.ini 安全配置

ini
; === Session 基本安全 ===
session.use_only_cookies = 1     ; 仅通过 Cookie 传递 SID(防止 URL 注入)
session.use_strict_mode = 1       ; 严格模式,不接受未初始化的 SID(PHP 7.1+)
session.use_trans_sid = 0         ; 禁用 URL 传递 SID

; === Cookie 安全 ===
session.cookie_httponly = 1      ; HttpOnly(防止 JS 访问)
session.cookie_secure = 1         ; Secure(仅 HTTPS 传输)
session.cookie_samesite = Lax      ; SameSite 属性(PHP 7.3+)
session.cookie_path = /           ; Cookie 路径
session.cookie_domain =           ; Cookie 域(子域名共享时设置)
session.cookie_lifetime = 0       ; Session Cookie(浏览器关闭时过期)

; === Session ID 安全 ===
session.sid_length = 48           ; Session ID 长度(PHP 7.1+,默认 32)
session.sid_bits_per_character = 6; 每个字符的位数(PHP 7.1+,默认 5)
; 48 位 / 6 = 8 个字符(a-zA-Z0-9,-)
; 总熵值 = 48 * 6 = 288 位

; === Session 生命周期 ===
session.gc_maxlifetime = 7200      ; Session 过期时间(秒)
session.gc_probability = 1        ; GC 触发概率分子
session.gc_divisor = 100           ; GC 触发概率分母(1/100)
session.cache_limiter = nocache    ; 禁用缓存(API 场景)

; === Session 存储 ===
session.save_handler = files       ; 文件存储
; session.save_handler = redis     ; Redis 存储(推荐生产环境)
session.save_path = /tmp          ; 存储(文件时)
; session.save_path = "tcp://127.0.0.1:6379" ; Redis 连接

运行时安全配置

php
<?php

declare(strict_types=1);

// 在 session_start() 之前配置
ini_set('session.use_only_cookies', '1');
ini_set('session.use_strict_mode', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1'); // 仅 HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.sid_length', '48');
ini_set('session.gc_maxlifetime', '7200');

// PHP 7.3+ 可直接设置 SameSite
ini_set('session.cookie_samesite', 'Strict');

session_start();

// 验证 Session ID 长度
$sidLength = strlen(session_id());
if ($sidLength < 32) {
    error_log("Session ID 长度不足: {$sidLength}");
}

PHP 8.1+ Session 配置改进

php
<?php

declare(strict_types=1);

// PHP 8.1+ 推荐使用 session_set_cookie_params()
// 设置更精细的 Cookie 参数

session_set_cookie_params([
    'lifetime' => 7200,        // 2 小时
    'path' => '/',
    'domain' => 'example.com', // 跨子域共享
    'secure' => true,           // 仅 HTTPS
    'httponly' => true,          // HttpOnly
    'samesite' => 'Strict',      // SameSite
]);

session_start();

// PHP 8.1+ Session ID 默认更长更安全
echo strlen(session_id()); // 48 字符

防 Session Fixation

php
<?php

declare(strict_types=1);

/**
 * Session Fixation 防御
 */

// === 方式一:session_regenerate_id() ===
session_start();

// 在用户身份变更时(如登录成功)重新生成 Session ID
function handleLogin(string $username, string $password): bool
{
    if (authenticate($username, $password)) {
        // 重新生成 Session ID,删除旧的
        session_regenerate_id(true);
        $_SESSION['user_id'] = getUserId($username);
        $_SESSION['authenticated'] = true;
        return true;
    }
    return false;
}

// === 方式二:use_strict_mode ===
// php.ini: session.use_strict_mode = 1
// 严格模式下,PHP 不会接受客户端发送的未初始化 Session ID
// 只会使用 PHP 自己生成的 Session ID

// 攻击流程对比:
// 无 strict mode:
// 1. 攻击者访问 ?PHPSESSID=evil123 -> 服务端创建 Session
// 2. 受害者使用 ?PHPSESSID=evil123 访问 -> 共享同一 Session
// 3. 受害者登录 -> 攻击者获得认证状态

// 有 strict mode:
// 1. 攻击者访问 ?PHPSESSID=evil123
// 2. PHP 忽略 evil123,生成新 Session ID
// 3. 受害者访问 ?PHPSESSID=evil123 -> PHP 忽略,生成新 Session
// 4. 攻击者无法绑定受害者的 Session

防 Session Hijacking

绑定客户端指纹

php
<?php

declare(strict_types=1);

/**
 * Session Hijacking 防御
 */

session_start();

function checkSessionIntegrity(): bool
{
    // 1. 检查 User-Agent
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    if (!isset($_SESSION['user_agent'])) {
        $_SESSION['user_agent'] = $ua;
    } elseif (!hash_equals($_SESSION['user_agent'], $ua)) {
        return false; // User-Agent 变化
    }

    // 2. 检查 IP(可选,NAT 环境可能不准确)
    $ip = $_SERVER['REMOTE_ADDR'];
    if (!isset($_SESSION['ip'])) {
        $_SESSION['ip'] = $ip;
    } elseif ($_SESSION['ip'] !== $ip) {
        // IP 变化,可能是 NAT 切换或被劫持
        // 可以记录但不立即拒绝
        $_SESSION['ip_changed'] = true;
    }

    // 3. 检查 Session 创建时间(超时检查)
    $maxLifetime = 7200; // 2 小时
    if (isset($_SESSION['created_at']) && time() - $_SESSION['created_at'] > $maxLifetime) {
        return false;
    }

    // 4. 检查最后活动时间(空闲超时)
    $idleTimeout = 1800; // 30 分钟无操作
    if (isset($_SESSION['last_activity']) && time() - $_SESSION['last_activity'] > $idleTimeout) {
        return false;
    }
    $_SESSION['last_activity'] = time();

    return true;
}

if (!checkSessionIntegrity()) {
    // Session 可能被劫持,强制重新登录
    session_destroy();
    header('Location: /login?reason=expired');
    exit;
}

HTTPS 强制

php
<?php

declare(strict_types=1);

// 所有使用 Session 的页面必须强制 HTTPS
function enforceHttps(): void
{
    if (
        (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'off')
        && ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') !== 'https'
    ) {
        $httpsUrl = 'https://' . ($_SERVER['HTTP_HOST'] ?? '')
            . ($_SERVER['REQUEST_URI'] ?? '/');
        http_response_code(301);
        header('Location: ' . $httpsUrl);
        exit;
    }
}

enforceHttps();
session_start();

Session 管理

安全的 Session 初始化

php
<?php

declare(strict_types=1);

class SecureSession
{
    public static function start(): bool
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            return true;
        }

        // 安全配置
        ini_set('session.use_only_cookies', '1');
        ini_set('session.use_strict_mode', '1');
        ini_set('session.cookie_httponly', '1');

        if (self::isHttps()) {
            ini_set('session.cookie_secure', '1');
        }

        // PHP 7.3+
        ini_set('session.cookie_samesite', 'Lax');

        return session_start();
    }

    public static function regenerate(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            return;
        }
        session_regenerate_id(true);
    }

    public static function destroy(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            return;
        }

        $_SESSION = [];

        if (ini_get('session.use_cookies')) {
            $params = session_get_cookie_params();
            setcookie(
                session_name(),
                '',
                time() - 42000,
                $params['path'],
                $params['domain'],
                $params['secure'],
                $params['httponly']
            );
        }

        session_destroy();
    }

    public static function set(string $key, mixed $value): void
    {
        $_SESSION[$key] = $value;
    }

    public static function get(string $key, mixed $default = null): mixed
    {
        return $_SESSION[$key] ?? $default;
    }

    public static function has(string $key): bool
    {
        return isset($_SESSION[$key]);
    }

    public static function remove(string $key): void
    {
        unset($_SESSION[$key]);
    }

    private static function isHttps(): bool
    {
        return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
            || ($_SERVER['SERVER_PORT'] ?? 80) == 443;
    }
}

// 使用
SecureSession::start();

// 登录
SecureSession::set('user_id', $userId);
SecureSession::set('username', $username);
SecureSession::regenerate();

// 获取
$userId = SecureSession::get('user_id');

// 登出
SecureSession::destroy();

Session 存储安全

php
<?php

// === 使用 Redis 存储 Session(推荐生产环境)===
// php.ini:
// session.save_handler = redis
// session.save_path = "tcp://127.0.0.1:6379?auth=yourpassword"

// === 使用数据库存储 Session ===
class SessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;

    public function open(string $path, string $name): bool
    {
        return true;
    }

    public function close(): bool
    {
        return true;
    }

    public function read(string $id): string|false
    {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires_at > NOW()'
        );
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? $row['data'] : '';
    }

    public function write(string $id, string $data): bool
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO sessions (id, data, expires_at, last_access)
             VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 2 HOUR), NOW())
             ON DUPLICATE KEY UPDATE data = VALUES(data),
             expires_at = VALUES(expires_at), last_access = NOW()'
        );
        return $stmt->execute([$id, $data]);
    }

    public function destroy(string $id): bool
    {
        $stmt = $this->pdo->prepare('DELETE FROM sessions WHERE id = ?');
        return $stmt->execute([$id]);
    }

    public function gc(int $max_lifetime): int|false
    {
        $stmt = $this->pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
        return $stmt->execute() ? $stmt->rowCount() : false;
    }
}

// 注册自定义 Session 处理器
// $handler = new SessionHandler($pdo);
// session_set_save_handler($handler, true);
// session_start();

注意事项

1. 不要在 URL 中传递 Session ID

php

// 错误:URL 传递 SID
// php.ini: session.use_trans_sid = 1(必须关闭)
// <a href="/page.php?PHPSESSID=abc123">Link</a>

// 正确:仅通过 Cookie 传递
// php.ini: session.use_trans_sid = 0
// php.ini: session.use_only_cookies = 1

2. 锁竞争问题

php
<?php

// 默认文件 Session 处理器使用文件锁
// 同一用户的并发请求会串行执行
// 对于高并发应用,使用 Redis 等替代方案

// 并发请求解决方案:
// 1. 使用 Redis 存储 Session
// 2. 使用 session_write_close() 尽早释放锁
session_start();
// 读取 Session 数据
$userId = $_SESSION['user_id'];
// 尽早关闭写入锁
session_write_close();
// 执行耗时操作...

最佳实践

1. Session 安全配置清单

php
<?php

// [x] use_only_cookies = 1
// [x] use_strict_mode = 1
// [x] cookie_httponly = 1
// [x] cookie_secure = 1(HTTPS 环境)
// [x] cookie_samesite = Lax/Strict
// [x] sid_length >= 48
// [x] gc_maxlifetime 设置合理过期时间
// [x] 登录后 session_regenerate_id(true)
// [x] 登出时完全销毁 Session
// [x] 使用 HTTPS 传输

下一节

继续学习:数据库安全

参考链接