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

实现流程
- 用户提交账号密码,勾选「记住我」
- 后端校验账号,比对密码哈希(数据库只存hash,不存明文)
- 根据remember勾选状态,设置不同的Token过期时间
- 后端生成随机Token,存入user_token数据表
- 将Token写入HttpOnly Cookie返回给浏览器
- 后续请求浏览器自动携带该Cookie
- 鉴权中间件读取Cookie中的Token,查询数据库校验有效性
- 校验通过,将user_id注入请求对象,控制器直接获取登录用户
- 退出登录:删除数据库Token + 清除Cookie
- 修改密码:删除该用户全部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;登录控制器:
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发出去了,接下来每个需要登录的请求都要校验
用中间件统一处理,控制器里就不用重复写鉴权逻辑
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<?phpreturn [ 'auth' => \app\middleware\AuthCheck::class,];路由里给需要保护的分组加上中间件:
// route/app.php<?phpuse 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();避坑指南
禁止localStorage存Token
XSS会直接偷走,Token放 HttpOnly Cookie
生产环境必须HTTPS
否则 Secure 属性不生效,Cookie传输可能被抓包
Token 必须设过期时间
不要永久有效,长效Token 也要有上限(比如 30 天)
改密码销毁全部旧Token
所有设备强制下线,这是安全基线
鉴权中间件只作用于需要登录的路由
登录接口放行,不要设全局中间件
Cookie开启Secure + HttpOnly + SameSite
