PHP实现Remember me功能 读取Cookie哈希密码登录报密码错误
问题根因
自动登录校验失败的直接原因是password_verify()函数使用逻辑错误:
- 该函数仅用于校验明文密码与密码哈希的匹配关系,要求第一个参数传入用户输入的明文密码,第二个参数传入存储的密码哈希。
- 你当前存入Cookie的
pwd字段是从数据库取出的密码哈希,自动登录时将这个哈希值作为第一个参数传入password_verify(),相当于把哈希值当作"明文"和同一份哈希做匹配,必然永远返回false,触发密码错误提示。
除此之外,原设计思路本身存在不可接受的安全风险:即使修改为直接比对Cookie与数据库中的密码哈希是否相等,一旦Cookie被窃取、或者数据库发生拖库,攻击者拿到密码哈希后就可以长期伪造用户身份登录,直到用户主动修改密码,完全不符合安全开发规范。
安全合规的Remember Me实现方案
核心原则:绝对不在Cookie中存储任何与用户密码相关的内容(包括明文、密码哈希),改用一次性随机持久登录令牌实现自动登录,令牌仅用于自动登录校验,和用户密码完全解耦。
前置准备:新建令牌存储表
单独建表存储持久登录令牌,避免和用户账号表耦合,方便令牌的作废、轮换管理:
CREATE TABLE `auth_tokens` ( `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` int(11) NOT NULL, `token_hash` varchar(255) NOT NULL, `expires_at` datetime NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段说明:
user_id:关联accounts表的用户ID,用户删除时自动级联删除对应令牌token_hash:存储令牌的哈希值,即使数据库泄露,攻击者也无法拿到可用的原始令牌expires_at:令牌过期时间,过期令牌自动失效
第一步:修改登录成功后的令牌下发逻辑(authenticate.php)
用户校验通过、勾选记住我选项时,执行以下逻辑替换原有存密码哈希到Cookie的代码:
- 生成密码学安全的高熵随机令牌,禁止用时间戳、弱随机函数生成
- 对令牌做哈希后存入数据库,绑定用户ID和过期时间
- 将用户标识(邮箱)、原始未哈希的令牌写入Cookie,同时开启HttpOnly属性禁止JS读取,HTTPS站点需额外开启Secure属性
替换代码片段:
// 原有校验密码通过、初始化会话的逻辑保留,替换原来的setcookie存pwd部分 if($_POST['remember'] == 1 || $_POST['remember'] == "on") { // 生成32字节安全随机令牌 $rawToken = bin2hex(random_bytes(32)); $expireTime = time() + 86400 * 30; // 30天有效期 $expireDt = date('Y-m-d H:i:s', $expireTime); // 哈希令牌后存入数据库 $tokenHash = password_hash($rawToken, PASSWORD_DEFAULT); $tokenStmt = $con->prepare('INSERT INTO auth_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)'); $tokenStmt->bind_param('iss', $id, $tokenHash, $expireDt); $tokenStmt->execute(); $tokenStmt->close(); // 写入Cookie,最后一个参数true开启HttpOnly,HTTPS站点请将第6个参数设为true开启Secure setcookie('remember_user', $_POST['email'], $expireTime, "/", "", false, true); setcookie('remember_token', $rawToken, $expireTime, "/", "", false, true); }
第二步:修改首页自动登录校验逻辑(index.php)
页面初始化时按优先级校验:先判断是否已有有效登录会话,有则直接跳转首页;没有会话再检测记住我Cookie做自动登录校验,校验流程为:
- 读取记住我Cookie,根据用户标识查询对应用户信息
- 查询该用户名下未过期的令牌记录
- 校验Cookie中携带的原始令牌与数据库存储的令牌哈希是否匹配
- 校验通过则初始化登录会话,可选做令牌轮换(作废旧令牌、下发新令牌)后跳转首页
- 校验失败则清除无效Cookie,展示正常登录表单
替换index.php开头的PHP逻辑代码:
<?php session_start(); // 已登录直接跳转首页 if(isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === TRUE) { header('Location: php/home.php'); exit; } $DATABASE_HOST = 'localhost'; $DATABASE_USER = 'root'; $DATABASE_PASS = ''; $DATABASE_NAME = 'webproject1'; $con = mysqli_connect($DATABASE_HOST, $DATABASE_USER, $DATABASE_PASS, $DATABASE_NAME); if (mysqli_connect_errno()) { exit('Failed to connect to MySQL: ' . mysqli_connect_error()); } // 检测记住我Cookie if(isset($_COOKIE['remember_user'], $_COOKIE['remember_token'])) { $rememberEmail = $_COOKIE['remember_user']; $rememberToken = $_COOKIE['remember_token']; if($stmt = $con->prepare('SELECT id FROM accounts WHERE email = ?')) { $stmt->bind_param('s', $rememberEmail); $stmt->execute(); $stmt->store_result(); if($stmt->num_rows > 0) { $stmt->bind_result($id); $stmt->fetch(); $stmt->close(); // 查询用户未过期的令牌 $tokenStmt = $con->prepare('SELECT id, token_hash FROM auth_tokens WHERE user_id = ? AND expires_at > NOW() LIMIT 1'); $tokenStmt->bind_param('i', $id); $tokenStmt->execute(); $tokenStmt->store_result(); if($tokenStmt->num_rows > 0) { $tokenStmt->bind_result($tokenId, $tokenHash); $tokenStmt->fetch(); $tokenStmt->close(); // 校验令牌匹配 if(password_verify($rememberToken, $tokenHash)) { // 初始化登录会话 session_regenerate_id(); $_SESSION['loggedin'] = TRUE; $_SESSION['name'] = $rememberEmail; $_SESSION['id'] = $id; // 令牌轮换:删除旧令牌,生成新令牌,降低令牌泄露风险 $delStmt = $con->prepare('DELETE FROM auth_tokens WHERE id = ?'); $delStmt->bind_param('i', $tokenId); $delStmt->execute(); $delStmt->close(); $newToken = bin2hex(random_bytes(32)); $newExpire = time() + 86400*30; $newExpireDt = date('Y-m-d H:i:s', $newExpire); $newTokenHash = password_hash($newToken, PASSWORD_DEFAULT); $newTokenStmt = $con->prepare('INSERT INTO auth_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)'); $newTokenStmt->bind_param('iss', $id, $newTokenHash, $newExpireDt); $newTokenStmt->execute(); $newTokenStmt->close(); setcookie('remember_user', $rememberEmail, $newExpire, "/", "", false, true); setcookie('remember_token', $newToken, $newExpire, "/", "", false, true); header('Location: php/home.php'); exit; } } $tokenStmt->close(); } else { $stmt->close(); } } // 校验失败,清除无效Cookie setcookie('remember_user', '', time() - 3600, "/"); setcookie('remember_token', '', time() - 3600, "/"); } ?> <!-- 原有登录页HTML代码保留即可 -->
第三步:补充令牌作废逻辑
在以下场景必须对应用户的有效令牌做删除处理,避免被盗令牌被恶意使用:
- 用户主动点击退出登录时,清除当前Cookie对应的令牌,或清除该用户所有持久令牌
- 用户修改密码、重置密码时,清除该用户名下所有持久登录令牌
退出逻辑参考代码:
<?php session_start(); // 数据库连接逻辑省略 if(isset($_COOKIE['remember_user'], $_COOKIE['remember_token'])) { // 可选择删除对应用户所有令牌,或仅删除当前Cookie对应的令牌 setcookie('remember_user', '', time() - 3600, "/"); setcookie('remember_token', '', time() - 3600, "/"); } session_destroy(); header('Location: index.php'); exit; ?>
安全注意事项
- 所有持久令牌必须设置合理有效期,禁止生成永久有效令牌
- Cookie必须开启HttpOnly属性,HTTPS站点必须开启Secure属性,建议设置SameSite=Lax/Strict,降低XSS、CSRF风险
- 令牌必须使用
random_bytes等密码学安全的随机数生成函数生成,禁止使用时间戳、uniqid()等弱随机源 - 数据库中仅存储令牌的哈希值,不存原始令牌
内容的提问来源于stack exchange,提问作者t3nsa
相关产品推荐
相关产品推荐

