Session 安全
Session 安全是 Web 应用安全的重要组成部分。Session 劫持、Session 固定和 Session 预测是常见的攻击方式。本节讲解 PHP Session 的安全配置、常见攻击的防御策略,以及安全 Session 管理的最佳实践。
前置知识
阅读本节前,建议先了解:Session 基础、安全总则
基础概念
Session 攻击类型
| 攻击类型 | 原理 | 防御方式 |
|---|---|---|
| Session Fixation | 攻击者预设 Session ID | session_regenerate_id() |
| Session Hijacking | 窃取有效 Session ID | HTTPS + 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 = 12. 锁竞争问题
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 传输下一节
继续学习:数据库安全