网站登录页的 记住我 是怎么实现的?

几乎每个登录页面都有一个「记住我」复选框
「记住我」到底是怎么实现的呢?今天从原理到完整代码,一次讲透
「记住我」不保存密码,保存的是登录凭证(Token)
勾选和不勾选的区别,只是凭证的有效期不同:
- 不勾选:下发短期 Token,关闭浏览器后会话结束,下次需要重新登录
- 勾选「记住我」:下发长效 Token(比如 30 天),关闭浏览器再打开,依然保持登录状态
实现流程
- 用户提交账号密码,勾选「记住我」;
- 后端校验账号,比对密码哈希(数据库只存 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 的关键
前端实现
<pre”><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 <?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
以上关于网站登录页的 记住我 是怎么实现的?的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 网站登录页的 记住我 是怎么实现的?
微信
支付宝