本文目录导读:

我来详细介绍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
]
这个实现提供了安全且完整的"记住我"功能,确保用户会话安全。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播