记住登入帐号 Remember Me

验证进阶知识: 记住登入帐号

一般登入流程

传统登入方式是透过帐号密码登入后,透过 cookie 中的 session_id 去与 Server 的 session 资料去做比对,如果有比对到 session 资料的话,会从储存在 session 中的使用者编号,去资料库或快取捞取使用者的资料,以达到登入该使用者的流程

步骤 流程 用途
1 传入帐号密码验证 验证帐号密码是否正确,资料库是否有此帐号
2 记录使用者资料及 session_id 到 Session 将登入使用者的资料储存在 Session,之后其他 Request 可以直接取用,避免敏感资料让其他使用者资料
3 记录使用者 session_id 到 Cookie 告诉前端此使用者的身份是谁,之后会透过这个含有 session_id 的 cookie 做验证
4 透过 session_id 的 Cookie 做其他验证请求 捞取 Cookie 中的 session_id 验证 Server 的 Session 是否有此 session_id,没有的话则表示未登入

帐号登入必要条件

要确认帐号有登入有两个条件

  1. 使用者含有 session_id 的 Cookie 存在浏览器
  2. Server 的 Session 中有此 session_id

要确保 Client 端的含有 session_id 的 Cookie 一直存在,这样 Server 才能有 session_id 资料去做验证

而且 Server 的 Session 有此 session_id 的使用者资料

Session 限制

因为 Cookie 是存在使用者自己的浏览器端,所以要确保 Client 端的含有 session_id 的 Cookie 一直存在比较容易,看系统安全性的情境,可以将 Cookie 的过期时间设定很长即可,例如 1 年、3 年、5 年之类的,因为存再久也只是存在使用者自己的浏览器,对于 Server 是几乎没有任何负担的

额外的负担应该就是每次 Request 都会把 cookie 传送到 Server,Cookie 越多的话,每个 Request 需要传送的资料就会越多,造成 Request 变得比较肥大,但这个就是取捨

但 Session 因为是存放在 Server 端,所以如果要完整地将使用者资料保留 1 年、3 年、5 年之类的负担会很大,如果使用者一年只登入一次,我们却要将他资料保留这麽久都没用到,而当使用者越来越多达到百万千万级时,一个 Session 档案虽然只有几 k,但一乘以百万千万来说,对于硬体的储存负担还是很大的

所以 Session 的资料通常会依照使用者的情境,不会将 Session 储存太久

记住登入帐号

为了减轻 Server 储存 Session 的压力,会在使用者资料表加入一个 remember_token 的栏位,当作帮使用者做重新产生 Session 登入的动作,所以登入流程会变成

步骤 流程 用途
1 传入帐号密码验证 验证帐号密码是否正确,资料库是否有此帐号
2 记录使用者资料及 session_id 到 Session 将登入使用者的资料储存在 Session,之后其他 Request 可以直接取用,避免敏感资料让其他使用者资料
3 记录使用者 session_id 到 Cookie 告诉前端此使用者的身份是谁,之后会透过这个含有 session_id 的 cookie 做验证
4 产生新的 remember_token 记录到使用者资料表的 remember_token 栏位 做为之后验证重新登入用
5 记录使用者 remember_token 及 user_id 到 Cookie 当 Cookie 中的 session_id 找不到 Session 时,会用 user_id 及 remember_token 去验证登入
6 透过 session_id 的 Cookie 做其他验证请求 捞取 Cookie 中的 session_id 验证 Server 的 Session 是否有此 session_id,没有的话则表示未登入
7 透过 remember_token 及 user_id 的 Cookie 做重新登入 捞取 Cookie 中的 remember_token 及 user_id,与资料库做比对,确认是否 token 合法可以正常登入,登入成功重新产生 Session

当 Session 保留时间很短时(例如 20 分钟),在 Session 移除时,也可以透过 remember_token 的 Cookie 去验重新登入的动作,并重新产生使用者的 Session,确保存放在 Server 中的 Session 都是近期登入的活跃使用者,确保不会有没在使用的 Session,也可以让使用者可以正常登入

remember_token 安全性

因为只要 Cookie 过期时间设定的够长,只要一直有 remember_token,则使用者就可以一直无限期的一直不断的维持登入状态,这样可能会有安全性的问题,所以在使用者自己触发登出时,记得更新储存在资料库的 remember_token,让其他有此 remember_token 的使用者无法继续登入

以 Laravel 记住登入举例

Laravel 版本 8.x

登入纪录登入帐号

在 Laravel 原生记录使用者 vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php 程式中,在 login() 函式的第二个参数是 $remember = false,若传入 true 则会对使用者进行记录登入帐号 token 的流程

// vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
/**
 * Log a user into the application.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
 * @param  bool  $remember
 * @return void
 */
public function login(AuthenticatableContract $user, $remember = false)
{
    // If the user should be permanently "remembered" by the application we will
    // queue a permanent cookie that contains the encrypted copy of the user
    // identifier. We will then decrypt this later to retrieve the users.
    if ($remember) {
        // 确保资料库有此 remember_token,若没有则产生新的
        $this->ensureRememberTokenIsSet($user);

        // 纪录 remember_token 到 cookie
        $this->queueRecallerCookie($user);
    }
}

ensureRememberTokenIsSet() 函式会确保资料库有产生 remember_token,如果没有的话则会重新产生一个 remember_token 储存至资料库

产生完 remember_token 后,则呼叫 queueRecallerCookie() 纪录 remember_token 到 cookie

在 Laravel 原生记录 Remember Token vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php 程式中,在 queueRecallerCookie() 会纪录此 Rememver Token

  • 第 1 个变数 $user->getAuthIdentifier() 是 user_id
  • 第 2 个变数 $user->getRememberToken() 是资料库的 remember_token
  • 第 3 个变数 $user->getAuthPassword() 是使用的密码

第 3 个参数在 Laravel 8.x 版本都没有用到,不确定纪录这个用途是要干嘛

// vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
/**
 * Queue the recaller cookie into the cookie jar.
 *
 * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
 * @return void
 */
protected function queueRecallerCookie(AuthenticatableContract $user)
{
    $this->getCookieJar()->queue($this->createRecaller(
        $user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.$user->getAuthPassword()
    ));
}

取得验证使用者资料

在使用 auth()->user() 捞取登入使用者资料时,会先使用 Session 试着捞取登入使用者的资料,当捞取不到 Session 中使用者的资料时,如果有 Remember Token,则会使用 Remember Token 去做登入

// vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php
/**
 * Get the currently authenticated user.
 *
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function user()
{
    // 取得 Session 中的使用者编号,使用 Session 登入
    $id = $this->session->get($this->getName());

    // First we will try to load the user using the identifier in the session if
    // one exists. Otherwise we will check for a "remember me" cookie in this
    // request, and if one exists, attempt to retrieve the user using that.
    if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }

    // 使用 Remember Token 登入使用者(如果有的话)
    // If the user is null, but we decrypt a "recaller" cookie we can attempt to
    // pull the user data on that cookie which serves as a remember cookie on
    // the application. Once we have a user we can return it to the caller.
    if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
        $this->user = $this->userFromRecaller($recaller);

        if ($this->user) {
            $this->updateSession($this->user->getAuthIdentifier());

            $this->fireLoginEvent($this->user, true);
        }
    }

    return $this->user;
}

参考资料