「记住我」功能原理与实现
登录页面的「记住我」功能并非保存用户密码,而是存储登录凭证(Token)。其核心区别在于凭证的有效期:未勾选时,下发短期 Token,会话随浏览器关闭而结束;勾选后,下发长效 Token(如 30 天),即使关闭浏览器再打开,用户仍保持登录状态。
核心实现流程
- 用户提交账号密码及「记住我」状态。
- 后端校验账号,比对数据库中的密码哈希值。
- 根据勾选状态设定 Token 过期时间。
- 生成随机 Token 并存入 user_token 数据表。
- 将 Token 写入 HttpOnly 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(),数据库仅存储 hash 值。
- Token 有效期由 remember 参数动态决定。
- Cookie 必须设置 HttpOnly=true,这是防御 XSS 攻击窃取 Token 的关键。
前端交互实现
前端表单需包含「记住我」复选框,并在登录成功后根据用户选择决定是否在 LocalStorage 缓存用户名以便下次回填:
<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>
鉴权中间件设计
为避免在控制器中重复编写鉴权逻辑,应使用中间件统一处理。优先校验 Session,若失效则尝试通过 Cookie 中的 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
<?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');
控制器中可直接通过$request->user_id获取当前用户 ID。
退出登录与安全策略
退出登录逻辑
退出时需同时清理 Session、数据库 Token 及 Cookie:
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 属性以增强安全性。

