大数跨境

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

登录页的「记住我」是怎么实现的? wordpress知识
2026-09-20
3

「记住我」功能原理与实现

登录页面的「记住我」功能并非保存用户密码,而是存储登录凭证(Token)。其核心区别在于凭证的有效期:未勾选时,下发短期 Token,会话随浏览器关闭而结束;勾选后,下发长效 Token(如 30 天),即使关闭浏览器再打开,用户仍保持登录状态。

核心实现流程

  1. 用户提交账号密码及「记住我」状态。
  2. 后端校验账号,比对数据库中的密码哈希值。
  3. 根据勾选状态设定 Token 过期时间
  4. 生成随机 Token 并存入 user_token 数据表。
  5. 将 Token 写入 HttpOnly Cookie 返回给浏览器。
  6. 后续请求自动携带该 Cookie。
  7. 鉴权中间件读取 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(),数据库仅存储 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 属性以增强安全性。
【声明】内容源于网络
0
0
wordpress知识
各类跨境出海行业相关资讯
内容 355
粉丝 0
wordpress知识 各类跨境出海行业相关资讯
总阅读10.1k
粉丝0
内容355