本文目录导读:

我来详细介绍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
]
这个实现提供了安全且完整的"记住我"功能,确保用户会话安全。