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

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

2026年10月05日 15:13 技术杂谈 55 阅读

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

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

核心原理

「记住我」不保存密码,保存的是登录凭证(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,所有设备强制下线。

后端实现

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();

避坑指南

  • 禁止 localStorage 存 Token:XSS 会直接偷走,Token 放 HttpOnly Cookie。
  • 生产环境必须 HTTPS:否则 Secure 属性不生效,Cookie 传输可能被抓包。
  • Token 必须设过期时间:不要永久有效,长效 Token 也要有上限(比如 30 天)。
  • 改密码销毁全部旧 Token:所有设备强制下线,这是安全基线。
  • 鉴权中间件只作用于需要登录的路由:登录接口放行,不要设全局中间件。
  • Cookie 开启 Secure + HttpOnly + SameSite。