PHP 记住我功能怎么安全

wen PHP项目 3

本文目录导读:

PHP 记住我功能怎么安全

  1. 安全实现方案
  2. 额外安全措施
  3. 最佳实践总结

我来详细介绍PHP中实现安全的"记住我"功能的最佳实践。

安全实现方案

核心安全原则

  • 不直接存储用户密码或敏感信息
  • 使用随机令牌而非用户ID
  • 令牌需要加密存储
  • 设置合理的过期时间

完整实现代码

<?php
class RememberMe {
    private $db;
    private $cookieName = 'remember_token';
    private $cookieExpiry = 30 * 24 * 60 * 60; // 30天
    private $tokenLength = 64;
    public function __construct($db) {
        $this->db = $db;
    }
    // 1. 登录时创建记住我令牌
    public function createToken($userId) {
        // 生成随机令牌
        $token = bin2hex(random_bytes($this->tokenLength));
        // 创建令牌的SHA256哈希用于数据库存储
        $tokenHash = hash('sha256', $token);
        // 设置过期时间
        $expires = date('Y-m-d H:i:s', time() + $this->cookieExpiry);
        // 存储到数据库(使用预处理语句防止SQL注入)
        $stmt = $this->db->prepare(
            "INSERT INTO remember_tokens (user_id, token_hash, expires_at) 
             VALUES (?, ?, ?)"
        );
        $stmt->execute([$userId, $tokenHash, $expires]);
        // 设置HTTP Only cookie
        setcookie(
            $this->cookieName,
            $userId . ':' . $token,
            [
                'expires' => time() + $this->cookieExpiry,
                'path' => '/',
                'secure' => true,     // 仅HTTPS
                'httponly' => true,   // 禁止JavaScript访问
                'samesite' => 'Strict' // 防CSRF
            ]
        );
        return true;
    }
    // 2. 验证记住我令牌
    public function validateToken() {
        if (!isset($_COOKIE[$this->cookieName])) {
            return false;
        }
        list($userId, $token) = explode(':', $_COOKIE[$this->cookieName]);
        // 计算令牌哈希
        $tokenHash = hash('sha256', $token);
        // 查询数据库
        $stmt = $this->db->prepare(
            "SELECT * FROM remember_tokens 
             WHERE user_id = ? AND token_hash = ? AND expires_at > NOW()"
        );
        $stmt->execute([$userId, $tokenHash]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($result) {
            // 验证成功后,轮换令牌(可选但推荐)
            $this->rotateToken($userId, $tokenHash);
            return $userId;
        }
        // 令牌无效,清除cookie
        $this->clearToken();
        return false;
    }
    // 3. 轮换令牌(每次使用时更新)
    private function rotateToken($userId, $oldTokenHash) {
        // 删除旧令牌
        $stmt = $this->db->prepare(
            "DELETE FROM remember_tokens WHERE user_id = ? AND token_hash = ?"
        );
        $stmt->execute([$userId, $oldTokenHash]);
        // 创建新令牌
        $this->createToken($userId);
    }
    // 4. 清除记住我(登出时调用)
    public function clearToken() {
        if (isset($_COOKIE[$this->cookieName])) {
            list($userId, $token) = explode(':', $_COOKIE[$this->cookieName]);
            $tokenHash = hash('sha256', $token);
            // 删除数据库记录
            $stmt = $this->db->prepare(
                "DELETE FROM remember_tokens WHERE user_id = ? AND token_hash = ?"
            );
            $stmt->execute([$userId, $tokenHash]);
            // 清除cookie
            setcookie($this->cookieName, '', [
                'expires' => time() - 3600,
                'path' => '/',
                'secure' => true,
                'httponly' => true,
                'samesite' => 'Strict'
            ]);
        }
    }
}

数据库表结构

CREATE TABLE remember_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    token_hash VARCHAR(64) NOT NULL,
    expires_at DATETIME NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_token_hash (token_hash),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

使用示例

<?php
// 登录处理
session_start();
if ($loginSuccess) {
    $rememberMe = new RememberMe($db);
    // 用户勾选"记住我"
    if (isset($_POST['remember_me'])) {
        $rememberMe->createToken($userId);
    }
    // 设置普通会话
    $_SESSION['user_id'] = $userId;
}
// 会话恢复
if (!isset($_SESSION['user_id'])) {
    $rememberMe = new RememberMe($db);
    $userId = $rememberMe->validateToken();
    if ($userId) {
        $_SESSION['user_id'] = $userId;
        session_regenerate_id(true); // 防会话固定攻击
    }
}
// 登出处理
if ($logout) {
    $rememberMe = new RememberMe($db);
    $rememberMe->clearToken();
    session_destroy();
}

额外安全措施

安全检测和防护

<?php
class EnhancedRememberMe extends RememberMe {
    // 检测异常使用
    public function validateWithDetection() {
        $result = $this->validateToken();
        if ($result) {
            $this->checkForAnomalies($_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT']);
        }
        return $result;
    }
    // 检查异常(IP变化等)
    private function checkForAnomalies($ip, $userAgent) {
        $stmt = $this->db->prepare(
            "SELECT last_ip, last_user_agent FROM users WHERE id = ?"
        );
        $stmt->execute([$this->getCurrentUserId()]);
        $userData = $stmt->fetch();
        // 如果IP或User-Agent变化过大,可能需要二次验证
        if ($userData['last_ip'] != $ip || 
            $userData['last_user_agent'] != $userAgent) {
            // 记录可疑行为
            $this->logSuspiciousActivity();
        }
        // 更新最后登录信息
        $updateStmt = $this->db->prepare(
            "UPDATE users SET last_ip = ?, last_user_agent = ? WHERE id = ?"
        );
        $updateStmt->execute([$ip, $userAgent, $this->getCurrentUserId()]);
    }
    // 清理过期令牌
    public function cleanupExpiredTokens() {
        $stmt = $this->db->prepare(
            "DELETE FROM remember_tokens WHERE expires_at < NOW()"
        );
        return $stmt->execute();
    }
}

最佳实践总结

必须做

  • ✅ 使用强随机数生成器(random_bytes
  • ✅ 数据库存储哈希而非原始令牌
  • ✅ 设置HttpOnly、Secure、SameSite属性
  • ✅ 定期清理过期令牌
  • ✅ 每次使用后轮换令牌

禁止做

  • ❌ 不要在cookie中存储用户ID
  • ❌ 不要使用可预测的令牌格式
  • ❌ 不要无限期有效
  • ❌ 不要存储原始令牌到数据库

推荐的Cookie设置

[
    'expires' => time() + 30*24*3600,  // 合理过期时间
    'path' => '/',
    'secure' => true,      // 生产环境必须HTTPS
    'httponly' => true,    // 防止XSS窃取
    'samesite' => 'Strict' // 防CSRF
]

这个实现提供了安全且完整的"记住我"功能,确保用户会话安全。

抱歉,评论功能暂时关闭!