文章内容
登录页的「记住我」是怎么实现的?
几乎每个登录页面都有一个「记住我」复选框
「记住我」到底是怎么实现的呢?今天从原理到完整代码,一次讲透。
核心原理
勾选和不勾选的区别,只是凭证的有效期不同:

实现流程
- 用户提交账号密码,勾选「记住我」。
- 后端校验账号,比对密码哈希(数据库只存 hash,不存明文)。
- 根据
remember勾选状态,设置不同的 Token 过期时间。 - 后端生成随机 Token,存入
user_token数据表。 - 将 Token 写入 HttpOnly Cookie 返回给浏览器。
- 后续请求浏览器自动携带该 Cookie。
- 鉴权中间件读取 Cookie 中的 Token,查询数据库校验有效性。
- 校验通过,将
user_id注入请求对象,控制器直接获取登录用户。 - 退出登录:删除数据库 Token + 清除 Cookie。
- 修改密码:删除该用户全部 Token,所有设备强制下线。
后端实现
1. 创建 user_token 表
先建一张 user_token 表,用来存每个用户的登录凭证。
CREATE TABLE `user_token` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL COMMENT '用户id',
`token` varchar(100) NOT NULL,
`expire_time` int NOT NULL COMMENT '过期时间戳',
`create_time` int NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `token` (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2. 登录控制器
public function login(Request $request)
{
$username = $request->post('username');
$password = $request->post('password');
$remember = $request->post('remember', 0);
$user = Db::name('user')->where('username', $username)->find();
if (!$user || !password_verify($password, $user['password'])) {
return json(['code' => 0, 'msg' => '账号密码错误']);
}
// 无论是否记住我,本次会话一定写入 Session
session('user_id', $user['id']);
// 勾选记住我:生成长效 Token
if ($remember) {
$expire = 30 * 24 * 3600;
$token = md5($user['id'] . time() . rand(1000, 9999));
Db::name('user_token')->insert([
'user_id' => $user['id'],
'token' => $token,
'expire_time' => time() + $expire,
'create_time' => time()
]);
// HttpOnly 长效 Cookie,防止 XSS 盗取
Cookie::set('remember_token', $token, $expire, '/', '', true, true);
} else {
// 不记住我,清除历史长效凭证
Cookie::delete('remember_token');
}
return json(['code' => 1, 'msg' => '登录成功']);
}注意:
- 密码校验用
password_verify(),数据库里存的是password_hash()生成的哈希值,不是明文。 - Token 有效期由
remember决定,勾选就长,不勾选就短。 - Cookie 设置
HttpOnly=true,这是防 XSS 偷 Token 的关键。
前端实现
<form id="loginForm">
<input type="text" name="username" placeholder="账号">
<input type="password" name="password" placeholder="密码">
<label>
<input type="checkbox" name="remember" value="1"> 记住我
</label>
<button type="submit">登录</button>
</form>
<script>
// 页面加载时回填用户名
window.onload = function () {
const uname = localStorage.getItem('username');
if (uname) {
document.querySelector('[name="username"]').value = uname;
}
};
document.getElementById('loginForm').onsubmit = async function (e) {
e.preventDefault();
let formData = new FormData(this);
let res = await fetch('/auth/login', {
method: 'POST',
body: formData
});
let data = await res.json();
if (data.code === 1) {
// 勾选记住我才存用户名,用于下次回填
if (formData.get('remember')) {
localStorage.setItem('username', formData.get('username'));
} else {
localStorage.removeItem('username');
}
location.href = '/index';
} else {
alert(data.msg);
}
};
</script>鉴权中间件
Token 发出去了,接下来每个需要登录的请求都要校验。
用中间件统一处理,控制器里就不用重复写鉴权逻辑。
<?php
namespace app\middleware;
use think\Request;
use think\facade\Db;
use think\facade\Cookie;
use think\Response;
class AuthCheck
{
public function handle(Request $request, \Closure $next): Response
{
// 1. 优先走 Session 鉴权
$uid = session('user_id');
if (!empty($uid)) {
$request->user_id = $uid;
return $next($request);
}
// 2. Session 失效,尝试用记住我 Token 恢复登录
$token = Cookie::get('remember_token');
if (empty($token)) {
return redirect('/login');
}
// 校验 Token 是否存在、是否过期
$info = Db::name('user_token')
->where('token', $token)
->where('expire_time', '>', time())
->find();
if (!$info) {
Cookie::delete('remember_token');
return redirect('/login');
}
// 自动重建 Session,恢复登录状态
session('user_id', $info['user_id']);
$request->user_id = $info['user_id'];
return $next($request);
}
}注册中间件别名,不要设成全局中间件——登录接口本身不需要鉴权,否则会死循环跳转:
// app/middleware.php
<?php
return [
'auth' => \app\middleware\AuthCheck::class,
];路由里给需要保护的分组加上中间件:
// route/app.php
<?php
use think\facade\Route;
// 登录相关,不需要鉴权
Route::post('auth/login', 'Auth/login');
Route::get('login', 'Auth/index');
// 需要登录保护的路由分组
Route::group(function () {
Route::get('index', 'Index/index');
// 其他后台接口...
})->middleware('auth');控制器里直接拿登录用户 ID:
public function index(Request $request)
{
$userId = $request->user_id;
return json(['user_id' => $userId]);
}退出登录和改密码强制下线
退出登录要做三件事:删 Token + 清 Cookie + 清 Session。
public function logout()
{
// 清空 Session
session_destroy();
// 销毁记住我 Token
$token = Cookie::get('remember_token');
if ($token) {
Db::name('user_token')->where('token', $token)->delete();
Cookie::delete('remember_token');
}
return redirect('/login');
}修改密码时,把该用户所有设备的 Token 全部删掉,实现「改密码后全部设备强制下线」:
// 修改密码成功后执行
Db::name('user_token')->where('user_id', $userId)->delete();