PHP会话与Cookies

学习如何使用PHP管理用户状态和持久化数据

Cookies基础

Cookies是存储在用户计算机上的小文本文件,用于在多次页面请求之间保持用户信息。由于HTTP协议是无状态的,Cookies提供了一种在客户端存储数据的机制。

Cookie的工作原理

当服务器向浏览器发送响应时,可以包含Set-Cookie头部,浏览器会存储这些Cookie。在后续的请求中,浏览器会自动将Cookie发送回服务器。

设置Cookie

使用setcookie()函数设置Cookie。必须在输出任何内容之前调用此函数。

设置Cookie
<?php
// 设置一个简单的Cookie
setcookie("user", "张三", time() + (86400 * 30), "/"); // 30天过期

// 设置带更多选项的Cookie
setcookie("theme", "dark", [
    'expires' => time() + 86400 * 30,
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'
]);

// 设置数组Cookie
setcookie("user[username]", "张三");
setcookie("user[email]", "zhangsan@example.com");
?>

读取Cookie

使用$_COOKIE超全局变量读取Cookie。

读取Cookie
<?php
if (isset($_COOKIE["user"])) {
    echo "欢迎回来, " . htmlspecialchars($_COOKIE["user"]) . "!";
} else {
    echo "欢迎新用户!";
}

// 读取数组Cookie
if (isset($_COOKIE['user'])) {
    echo "用户名: " . htmlspecialchars($_COOKIE['user']['username']);
    echo "邮箱: " . htmlspecialchars($_COOKIE['user']['email']);
}
?>

删除Cookie

通过设置过期时间为过去的时间来删除Cookie。

删除Cookie
<?php
// 删除Cookie
setcookie("user", "", time() - 3600, "/");

// 删除数组Cookie
setcookie("user[username]", "", time() - 3600);
setcookie("user[email]", "", time() - 3600);
?>

Cookie的限制和注意事项

  • 每个Cookie最大4KB
  • 每个域名最多20个Cookie(不同浏览器可能不同)
  • Cookie存储在客户端,用户可以查看和修改
  • 敏感数据不应存储在Cookie中
  • 设置适当的过期时间
  • 使用安全标志(Secure, HttpOnly, SameSite)

Session基础

Session是一种在服务器端存储用户信息的机制,比Cookie更安全。Session数据存储在服务器上,客户端只保存一个Session ID。

Session的工作原理

当Session启动时,PHP会生成一个唯一的Session ID,并通过Cookie发送给客户端。在后续请求中,客户端会发送这个Session ID,PHP根据ID恢复对应的Session数据。

启动Session

启动Session
<?php
// 启动Session
session_start();

// 设置Session变量
$_SESSION["username"] = "张三";
$_SESSION["user_id"] = 12345;
$_SESSION["last_login"] = date("Y-m-d H:i:s");
$_SESSION["preferences"] = [
    "theme" => "dark",
    "language" => "zh-CN",
    "timezone" => "Asia/Shanghai"
];

echo "Session已启动!";
?>

读取Session数据

读取Session
<?php
// 启动Session
session_start();

// 读取Session数据
if (isset($_SESSION["username"])) {
    echo "欢迎回来, " . htmlspecialchars($_SESSION["username"]) . "!<br>";
    echo "用户ID: " . $_SESSION["user_id"] . "<br>";
    echo "最后登录: " . $_SESSION["last_login"] . "<br>";
    
    // 读取数组Session数据
    echo "主题: " . htmlspecialchars($_SESSION["preferences"]["theme"]) . "<br>";
    echo "语言: " . htmlspecialchars($_SESSION["preferences"]["language"]) . "<br>";
} else {
    echo "请先登录!";
}
?>

销毁Session

销毁Session
<?php
// 启动Session
session_start();

// 销毁所有Session数据
session_unset();
session_destroy();

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

echo "Session已销毁!";
?>

Session垃圾回收

PHP会自动清理过期的Session文件。可以通过以下配置调整垃圾回收行为:

  • session.gc_maxlifetime - Session过期时间(秒)
  • session.gc_probability - 垃圾回收概率分子
  • session.gc_divisor - 垃圾回收概率分母

Session配置

可以通过session_set_cookie_params()php.ini文件配置Session。

Session配置选项

Session配置
<?php
// 设置Session Cookie参数
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'
]);

// 设置Session垃圾回收时间(24小时)
ini_set('session.gc_maxlifetime', 86400);

// 自定义Session存储路径
ini_set('session.save_path', '/custom/session/path');

// 启动Session
session_start();
?>

Session安全设置

Session安全
<?php
// 增强Session安全性
ini_set('session.use_strict_mode', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');

// 定期更换Session ID
session_start();
if (mt_rand(1, 100) <= 5) {
    session_regenerate_id(true);
}

// 自定义Session ID生成
session_id(bin2hex(random_bytes(16)));
?>

自定义Session处理器

可以自定义Session处理器,将Session数据存储到数据库、Redis等。

数据库Session处理器
<?php
class DBSessionHandler implements SessionHandlerInterface
{
    private $pdo;
    
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    
    public function open($savePath, $sessionName) {
        return true;
    }
    
    public function close() {
        return true;
    }
    
    public function read($sessionId) {
        $stmt = $this->pdo->prepare("SELECT session_data FROM sessions WHERE session_id = ?");
        $stmt->execute([$sessionId]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result ? $result['session_data'] : '';
    }
    
    public function write($sessionId, $sessionData) {
        $stmt = $this->pdo->prepare("REPLACE INTO sessions (session_id, session_data, last_accessed) VALUES (?, ?, ?)");
        return $stmt->execute([$sessionId, $sessionData, time()]);
    }
    
    public function destroy($sessionId) {
        $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE session_id = ?");
        return $stmt->execute([$sessionId]);
    }
    
    public function gc($maxLifetime) {
        $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE last_accessed < ?");
        return $stmt->execute([time() - $maxLifetime]);
    }
}

// 使用自定义Session处理器
$pdo = new PDO("mysql:host=localhost;dbname=test", "username", "password");
$handler = new DBSessionHandler($pdo);
session_set_save_handler($handler, true);
session_start();
?>

Cookie和Session的实际应用

用户登录系统

登录系统
<?php
session_start();

// 登录处理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
    $username = trim($_POST['username']);
    $password = $_POST['password'];
    
    // 验证用户(这里简化了验证过程)
    if (authenticateUser($username, $password)) {
        // 设置Session
        $_SESSION['user_id'] = getUserId($username);
        $_SESSION['username'] = $username;
        $_SESSION['logged_in'] = true;
        $_SESSION['login_time'] = time();
        
        // 设置"记住我"Cookie
        if (isset($_POST['remember_me'])) {
            $token = bin2hex(random_bytes(32));
            setRememberMeToken($_SESSION['user_id'], $token);
            setcookie('remember_me', $token, time() + (86400 * 30), '/', '', true, true);
        }
        
        header('Location: dashboard.php');
        exit;
    } else {
        $error = "用户名或密码错误";
    }
}

// 自动登录(通过"记住我"Cookie)
if (!isset($_SESSION['logged_in']) && isset($_COOKIE['remember_me'])) {
    $user_id = validateRememberMeToken($_COOKIE['remember_me']);
    if ($user_id) {
        $_SESSION['user_id'] = $user_id;
        $_SESSION['username'] = getUsername($user_id);
        $_SESSION['logged_in'] = true;
        $_SESSION['login_time'] = time();
    }
}

function authenticateUser($username, $password) {
    // 实际应用中应该查询数据库验证用户
    return ($username === 'admin' && $password === 'password');
}

function getUserId($username) {
    // 根据用户名获取用户ID
    return 1;
}

function setRememberMeToken($user_id, $token) {
    // 将token存储到数据库
    // 实际应用中应该哈希token并设置过期时间
}

function validateRememberMeToken($token) {
    // 验证token并返回用户ID
    // 实际应用中应该查询数据库验证token
    return 1;
}

function getUsername($user_id) {
    // 根据用户ID获取用户名
    return 'admin';
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>登录</title>
</head>
<body>
    <?php if (isset($error)): ?>
        <div style="color: red;"><?php echo htmlspecialchars($error); ?></div>
    <?php endif; ?>
    
    <form method="post">
        <div>
            <label>用户名:</label>
            <input type="text" name="username" required>
        </div>
        <div>
            <label>密码:</label>
            <input type="password" name="password" required>
        </div>
        <div>
            <label>
                <input type="checkbox" name="remember_me"> 记住我
            </label>
        </div>
        <div>
            <input type="submit" name="login" value="登录">
        </div>
    </form>
</body>
</html>

购物车系统

购物车实现
<?php
session_start();

// 初始化购物车
if (!isset($_SESSION['cart'])) {
    $_SESSION['cart'] = [];
}

// 添加商品到购物车
if (isset($_POST['add_to_cart'])) {
    $product_id = intval($_POST['product_id']);
    $quantity = intval($_POST['quantity']);
    
    if ($quantity > 0) {
        if (isset($_SESSION['cart'][$product_id])) {
            $_SESSION['cart'][$product_id] += $quantity;
        } else {
            $_SESSION['cart'][$product_id] = $quantity;
        }
    }
}

// 更新购物车数量
if (isset($_POST['update_cart'])) {
    foreach ($_POST['quantity'] as $product_id => $quantity) {
        $product_id = intval($product_id);
        $quantity = intval($quantity);
        
        if ($quantity > 0) {
            $_SESSION['cart'][$product_id] = $quantity;
        } else {
            unset($_SESSION['cart'][$product_id]);
        }
    }
}

// 从购物车移除商品
if (isset($_GET['remove'])) {
    $product_id = intval($_GET['remove']);
    unset($_SESSION['cart'][$product_id]);
}

// 清空购物车
if (isset($_POST['clear_cart'])) {
    $_SESSION['cart'] = [];
}

// 获取购物车商品总数
function getCartItemCount() {
    if (isset($_SESSION['cart'])) {
        return array_sum($_SESSION['cart']);
    }
    return 0;
}

// 获取购物车总价
function getCartTotal() {
    $total = 0;
    if (isset($_SESSION['cart'])) {
        foreach ($_SESSION['cart'] as $product_id => $quantity) {
            $price = getProductPrice($product_id);
            $total += $price * $quantity;
        }
    }
    return $total;
}

function getProductPrice($product_id) {
    // 实际应用中应该查询数据库获取价格
    $prices = [
        1 => 19.99,
        2 => 29.99,
        3 => 9.99
    ];
    return isset($prices[$product_id]) ? $prices[$product_id] : 0;
}

function getProductName($product_id) {
    // 实际应用中应该查询数据库获取商品名称
    $names = [
        1 => 'PHP教程',
        2 => 'JavaScript教程',
        3 => 'HTML教程'
    ];
    return isset($names[$product_id]) ? $names[$product_id] : '未知商品';
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>购物车</title>
</head>
<body>
    <h1>购物车 (<?php echo getCartItemCount(); ?> 件商品)</h1>
    
    <?php if (empty($_SESSION['cart'])): ?>
        <p>购物车为空</p>
    <?php else: ?>
        <form method="post">
            <table border="1">
                <tr>
                    <th>商品</th>
                    <th>单价</th>
                    <th>数量</th>
                    <th>小计</th>
                    <th>操作</th>
                </tr>
                <?php foreach ($_SESSION['cart'] as $product_id => $quantity): ?>
                    <tr>
                        <td><?php echo htmlspecialchars(getProductName($product_id)); ?></td>
                        <td><?php echo number_format(getProductPrice($product_id), 2); ?></td>
                        <td>
                            <input type="number" name="quantity[<?php echo $product_id; ?>]" 
                                   value="<?php echo $quantity; ?>" min="1">
                        </td>
                        <td><?php echo number_format(getProductPrice($product_id) * $quantity, 2); ?></td>
                        <td>
                            <a href="?remove=<?php echo $product_id; ?>">移除</a>
                        </td>
                    </tr>
                <?php endforeach; ?>
                <tr>
                    <td colspan="3">总计</td>
                    <td colspan="2"><?php echo number_format(getCartTotal(), 2); ?></td>
                </tr>
            </table>
            
            <button type="submit" name="update_cart">更新购物车</button>
            <button type="submit" name="clear_cart">清空购物车</button>
        </form>
    <?php endif; ?>
    
    <h2>添加商品</h2>
    <form method="post">
        <select name="product_id">
            <option value="1">PHP教程 - ¥19.99</option>
            <option value="2">JavaScript教程 - ¥29.99</option>
            <option value="3">HTML教程 - ¥9.99</option>
        </select>
        <input type="number" name="quantity" value="1" min="1">
        <button type="submit" name="add_to_cart">添加到购物车</button>
    </form>
</body>
</html>

Cookie和Session的安全考虑

常见安全威胁

  • 会话劫持: 攻击者获取用户的Session ID
  • 会话固定: 攻击者强制用户使用特定的Session ID
  • 跨站脚本攻击(XSS): 通过XSS窃取Session Cookie
  • 跨站请求伪造(CSRF): 利用用户的已认证状态执行非预期操作

安全最佳实践

Session安全配置
<?php
// Session安全配置
ini_set('session.use_strict_mode', 1);        // 只接受服务器生成的Session ID
ini_set('session.use_only_cookies', 1);       // 只使用Cookie存储Session ID
ini_set('session.cookie_httponly', 1);        // 防止JavaScript访问Session Cookie
ini_set('session.cookie_secure', 1);          // 仅通过HTTPS传输Session Cookie
ini_set('session.cookie_samesite', 'Strict'); // 防止CSRF攻击
ini_set('session.gc_maxlifetime', 1800);       // Session过期时间(30分钟)

// 启动Session
session_start();

// 定期更换Session ID
if (!isset($_SESSION['created'])) {
    $_SESSION['created'] = time();
} elseif (time() - $_SESSION['created'] > 300) {
    // 每5分钟更换一次Session ID
    session_regenerate_id(true);
    $_SESSION['created'] = time();
}

// 验证用户代理(防止会话劫持)
if (isset($_SESSION['user_agent'])) {
    if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        // 用户代理不匹配,可能是会话劫持
        session_unset();
        session_destroy();
        die("安全检测异常");
    }
} else {
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}

// 验证IP地址(可选,可能影响代理用户)
if (isset($_SESSION['ip_address'])) {
    if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
        // IP地址不匹配
        session_unset();
        session_destroy();
        die("安全检测异常");
    }
} else {
    $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
}
?>

安全Cookie设置

安全Cookie设置
<?php
// 设置安全Cookie
setcookie(
    "preferences",           // Cookie名称
    "dark_theme",            // Cookie值
    time() + (86400 * 30),   // 过期时间(30天)
    "/",                     // 路径
    "example.com",           // 域名
    true,                    // 仅通过HTTPS传输
    true                     // 仅HTTP访问,防止JavaScript访问
);

// 设置SameSite Cookie(防止CSRF)
header('Set-Cookie: preferences=dark_theme; expires=' . gmdate('D, d M Y H:i:s T', time() + 86400 * 30) . '; path=/; secure; httponly; SameSite=Strict');
?>

高级Session管理

分布式Session存储

在分布式系统中,Session数据需要共享存储,常见方案包括:

  • 数据库存储: 将Session数据存储在关系型数据库中
  • Redis存储: 使用Redis作为Session存储后端
  • Memcached存储: 使用Memcached存储Session数据

Redis Session处理器

Redis Session处理器
<?php
class RedisSessionHandler implements SessionHandlerInterface
{
    private $redis;
    private $prefix = 'sess:';
    private $ttl;
    
    public function __construct($redis, $ttl = 1800) {
        $this->redis = $redis;
        $this->ttl = $ttl;
    }
    
    public function open($savePath, $sessionName) {
        return true;
    }
    
    public function close() {
        return true;
    }
    
    public function read($sessionId) {
        $key = $this->prefix . $sessionId;
        $data = $this->redis->get($key);
        return $data ?: '';
    }
    
    public function write($sessionId, $sessionData) {
        $key = $this->prefix . $sessionId;
        return $this->redis->setex($key, $this->ttl, $sessionData);
    }
    
    public function destroy($sessionId) {
        $key = $this->prefix . $sessionId;
        return $this->redis->del($key) > 0;
    }
    
    public function gc($maxLifetime) {
        // Redis会自动处理过期键,这里不需要额外操作
        return true;
    }
}

// 使用Redis Session处理器
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$handler = new RedisSessionHandler($redis, 1800);
session_set_save_handler($handler, true);
session_start();
?>

Session性能优化

  • 避免在Session中存储大量数据
  • 使用适当的Session垃圾回收设置
  • 考虑使用内存存储(如Redis)提高性能
  • 对Session数据进行压缩
  • 使用Session锁避免并发问题

Cookie和Session的选择指南

场景 Cookie Session
存储位置 客户端 服务器端
安全性 较低(用户可查看和修改) 较高(数据存储在服务器)
存储容量 较小(每个Cookie 4KB,每个域名有限制) 较大(受服务器内存限制)
持久性 可设置长期有效 通常随浏览器关闭而失效
适用场景 用户偏好设置、跟踪信息、记住登录状态 用户登录状态、购物车、敏感数据
性能影响 每次请求都会发送,增加带宽 服务器需要存储和检索数据

最佳实践总结

  • 敏感数据使用Session存储
  • 非敏感的用户偏好使用Cookie存储
  • Session数据尽量精简
  • 设置合理的Session过期时间
  • 使用安全的Cookie标志(Secure, HttpOnly, SameSite)
  • 定期更换Session ID
  • 实施适当的Session安全措施