首页 技术杂谈 登录页的「记住我」是怎么实现的?
文章内容

登录页的「记住我」是怎么实现的?

2026年10月02日 14:03 技术杂谈 12 阅读

几乎每个登录页面都有一个「记住我」复选框

「记住我」到底是怎么实现的呢?今天从原理到完整代码,一次讲透

「记住我」不保存密码,保存的是登录凭证(Token)

勾选和不勾选的区别,只是凭证的有效期不同:

  • 不勾选:下发短期Token,关闭浏览器后会话结束,下次需要重新登录
  • 勾选「记住我」:下发长效Token(比如30天),关闭浏览器再打开,依然保持登录状态

实现流程

  1. 用户提交账号密码,勾选「记住我」
  2. 后端校验账号,比对密码哈希(数据库只存hash,不存明文)
  3. 根据remember勾选状态,设置不同的Token过期时间
  4. 后端生成随机Token,存入user_token数据表
  5. 将Token写入HttpOnly Cookie返回给浏览器
  6. 后续请求浏览器自动携带该Cookie
  7. 鉴权中间件读取Cookie中的Token,查询数据库校验有效性
  8. 校验通过,将user_id注入请求对象,控制器直接获取登录用户
  9. 退出登录:删除数据库Token + 清除Cookie
  10. 修改密码:删除该用户全部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