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

AI 概述
本文详解登录页「记住我」功能实现原理与完整代码。其核心是根据勾选状态分发**长短效Token**,不保存用户密码。后端建立令牌数据表,将长效Token存入HttpOnly Cookie;前端配合回填用户名。通过中间件实现Session与Token双重鉴权,退出或改密码可销毁对应令牌。同时总结安全避坑要点,保障登录体系安全可靠。
目录
文章目录隐藏
  1. 实现流程
  2. 后端实现
  3. 前端实现
  4. 鉴权中间件
  5. 退出登录和改密码强制下线
  6. 避坑指南

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

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

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

「记住我」不保存密码,保存的是登录凭证(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 的关键

前端实现

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

避坑指南

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

Cookie 开启 Secure + HttpOnly + SameSite

以上关于网站登录页的 记住我 是怎么实现的?的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

33

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 网站登录页的 记住我 是怎么实现的?

发表回复