记住登录的内部机制

一、概述

在使用浏览器登录某个系统时,我们经常会看到“记住登录”这个选项,一般我们会认为记住登录就是下次重新访问这个界面时,不需要再次输入用户名和密码,直接就进入系统。有的人会觉得这样做不安全,认为是自己的用户名和密码被记住了,但是他也不知道是怎么被记住,并且记在哪儿。

懂点web技术的同学知道是浏览器通过cookie保存了登录凭证,下次登录时浏览器会自动提交凭证,而对于使用者来说整个验证过程都是透明的,因为浏览器自动重定位页面速度非常快,一般人会觉得似乎没有访问过登录界面。

下面我就详细地给大家介绍一下记住登录的整个工作原理以及实现代码,希望大家能够对“记住登录”有一个清楚的认识,做好个人账户安全管理工作。

B/S(浏览器/服务器)应用也就是我们常说的web应用,一般通过浏览器进行使用,也有一些应用采用内嵌浏览器内核的方式提供给用户使用,这些都是一些直观上的感受。其核心在于使用HTTP协议进行信息交互的应用。

HTTP协议全称为超文本传输协议,包括1.1版本及之前的版本都是采用字符编码的报文,通信过程采用一问一答的方式,如下图所示,请求由客户端发出,服务端返回响应信息,之后如果还要进行通信,还是由客户端发出请求,服务端再返回新的请求。对于服务器和客户端来说,每一次的通信过程都是新的,中间不保留任何状态信息,也正是因为如此,大量的事务可以得到高效的处理。这就是HTTP无状态协议本质所在。

记住登录的内部机制_第1张图片
图片来自《图解HTTP》

因为早期的业务场景比较简单,但是随着后来业务的发展,必须要有一种机制,记住上一次通信状态信息,比如典型的用户管理系统,首次登录时用户输入用户名和密码进行验证,验证通过后就需要记住登录状态信息,避免在下次通信时重复验证过程。因此,HTTP协议中就引入了Cookie机制,即在每次通信的报文中加入一个Cookie字段,用于标识客户端身份。

记住登录的内部机制_第2张图片
图片来自《图解HTTP》

首次设置Cookie时,服务器会在报文头部加入Set-Cookie字段,同时,服务器会在自己的会话Session中记录下这个Cookie:

Set-Cookie: PHPSESSID=27r66cdl08m4jlmk7laufqddm0; path=/

Cookie各个属性说明

记住登录的内部机制_第3张图片
图片来自《图解HTTP》

客户端拿到这个Cookie后,在之后的整个请求报文中,都会加入Cookie字段:

Cookie: PHPSESSID=27r66cdl08m4jlmk7laufqddm0

我们可以通过浏览器开发人员工具进行查看:

记住登录的内部机制_第4张图片

服务端收到客户端的报文后,会自动从报文头部中提取出Cookie,并整理汇总与之关联的Session数据。

这里需要注意的是:
1.服务端会为每个客户端维护一份状态信息,叫做Session,客户端也针对每一个服务器维护一份状态信息,叫做Cookie。他们之间会通过一个唯一的Cookie值进行关联起来,比如本示例中的PHPSESSID。

2.服务器通过Set-Cookie来设置更新客户端的Cookie,但并不意味着双方的信息完全同步。一方面,服务器更新了自己的Seesion之后需要在报文头部加入Set-Cookie才能够更新客户端的Cookie,另一方面,我们可以手动清除客户端的Cookie,或者使用工具私自伪造修改Cookie。所以,这也是Web安全漏洞所在。

3.PHPSESSID是PHP引擎自动设置的一个Cookie,是全局唯一标识,标识与其通信的客户端,我们在PHP脚本中设置的全部Cookie都会与这个唯一标识对应起来,也就是说,当一个客户端请求过来时,引擎会自动的提取Cookie中的PHPSESSID,再从自己的Session中获取与之关联的Session值,初始化PHP脚本执行的全局环境,而作为脚本开发人员来说,无需关心哪个客户端对应那些Session。其实,所有的CGI程序都是如此。

二、登录状态信息维护

当我们访问需要登录的页面(URL)时,服务器会检查当前客户端是否登录,具体的做法是判断Session中记录登录状态的字段是否有效,如果有效就直接返回用户所要请求的页面;如果无效就强制客户端跳转到登录页面。

记住登录的内部机制_第5张图片

填写用户名和密码,向服务器提交请求。
服务器收到请求后,会从报文中拿到用户名和密码,并查询数据库验证其是否一致。

验证通过后,服务器会在当前的会话中记录下客户端已登录的信息,并返回用户之前请求的页面。

此后的通信过程,只要涉及到需要用户在登录后访问的页面,服务器都会从它自己的Seesion中检查用户的登录情况,而这Session与客户端的一一对应关系是由唯一的Cookie,在本示例中是PHPSESSID来实现的。

一般地,这个由CGI程序(PHP引擎)设置的Cookie有效期为0,也就是当浏览器关闭后Cookie自动失效清除,重新打开浏览器发送请求,服务端会重新分配一个Session,因此当我们关闭浏览器并重新打开访问页面时,就需要再次进行登录,这也就有了“记住登录”这个由来。
当然你也可以设置它的过期时间,PHP引擎的具体配置在php.ini文件中,下面给出Session的全部设置参数:

[Session]
session.save_handler = files
;session.save_path = "N;MODE;/path"
;session.save_path = "/tmp"
session.use_cookies = 1
;session.cookie_secure =
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
;session.entropy_length = 32
;session.entropy_file = /dev/urandom
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 0
session.hash_bits_per_character = 5
session.save_path="D:\phpStudy\tmp\tmp"
;session.upload_progress.enabled = On
;session.upload_progress.cleanup = On
;session.upload_progress.prefix = "upload_progress_"
;session.upload_progress.name = 
;session.upload_progress.freq =  "1%"
;session.upload_progress.min_freq = "1"

三、记住登录

记住登录主要为避免以下两种情况:

1.为了避免每次打开浏览器时都需要进行再次登录,我们就需要使用自己设置的Cookie进行记录,本次示例中有效时间设置为7,7之后可选择重新进行登录。

2.此外,服务器端的会话Seesion保存时间也是有要求的,PHP默认设置为24分钟,在php.ini配置文件中可以找到:

session.gc_maxlifetime = 1440

也就说在24分钟内,客户端没有向服务端发送请求,24分钟后,服务器会自动清除当前会话信息,那么当前的登录信息也会失效,用户必须重新进行登录。

因此我们在客户端保存一个自己分配的Cookie字段uid和remember_me

Cookie: PHPSESSID=cstlvkaq88a18d1dg63k4dccs5; remember_me=be59b7e7173438fe512004e238d66fd5

uid是用户的唯一id,remember_me是临时随机分配的128位字符串。
当前会话失效后,我们通过客户端Cookie中的uid和remember_me从数据库中查询用户的相关信息。

如果查找失败,就需要用户重新进行登录,跳转到登录页面。

如果查找成功,就说明用户已经的登录,为了安全起见,我们需要重新分配一个remember_me,并将其保存到数据库中,返回用户要访问的页面。整个过程如果执行的非常快,用户是察觉不到的,就好像自己直接通过验证并访问所需要的页面。

四、代码实现

对于所有需要登录的页面,我们统一使用一个基类控制器(基于ThinkPHP3.2框架)。

checkLogin();

        if($uid) {
            $this->uid = $uid;
        } else {
            if (IS_AJAX) {
                header('HTTP/1.1 404');
                $this->error('unlogin');
            } else {
                $this->redirect('Admin/Login/index');
            }
            
        }

        parent::__construct();
    }

    /**
     * 检测用户是否登录
     *
     * @return bool :true,已经登录;false,未登录
     */
    public function checkLogin()
    {
        // 获取服务器端的uid
        $uid = $_SESSION['uid'];

        if ($uid) {
            return $uid;
        }


        // 1 用户是否选择了记住登录
        // 1.1 如果用户没有选择记住登录,跳转到登录界面
        // 1.2 如果用户选择了记住登录,就要查询数据库,看客户端cookie中的token是否和数据库中的一致
        // 获取客户端的rememberme
        $rememberMe = cookie('remember_me');
        if (!$rememberMe) {
            return false;
        }



        // 1 去数据库中查询会话token
        // 1.1 如果数据库中存在
        // 1.1.1 获取用户信息,更新会话token,重写入服务器session,更新客户端的cookie
        // 1.1.2 将更新后的cookie写入数据库
        // 1.2 不存在,返回false

        // 去数据库中查询会话token
        $userid = cookie('uid');
        $userModel = M('user');

        $user = $userModel->field('id, password')->where("id='{$userid}' AND loginSessionId='{$rememberMe}'")->find();

        if ($user) {
            // 更新会话token
            $remember_me = md5($user->password.time());
            // 写入会话
            session('uid', $user['id']);
            session('uname', $user['nickname']);
            session('uavatar', $user['avatar']);
            session('uroles', $user['roles']);

            // 更新客户端的cookie
            cookie('remember_me', $remember_me, 3600*24*7);

            // 将更新后的remember_me
            $userModel->where("id={$user['id']}")->setField('loginSessionId', $remember_me);
        } else {
            return false;
        }

    }


}

登录过程处理

checkLogin();

        if($uid) {
            $this->success('用户已登录', $goto, 3);
        } else {
            $this->assign('goto', $goto);
            $this->display();
        }
    }

    public function ajax(){
        $this->display();
    }

    public function login()
    {
        $username = I('post.username', '');
        $password = I('post.password', '');
        $remember_me = I('post.remember_me', '');
        $goto = I('post.goto', '/');

        // 校验参数
        if (empty($username) || empty($password)) {
            $this->error('用户名或密码不能为空');
        }

        // 用户是否存在
        $userModel = M('user');
        $user =  $userModel->field('id, nickname, password, salt, locked, roles, avatar')->where("`nickname`='$username'")->find();

        // 用户不存在
        if (!$user) {
            $this->ajaxReturn(array(
                'success' => false,
                'message' => '用户不存在'
            ));
        }

        // 用户是否被禁用
        if ($user['locked'] > 0) {
            $this->ajaxReturn(array(
                'success' => false,
                'message' => '用户被禁用'
            ));
        }

        // 用户名和密码是否一致
        $password = md5($user['nickname'].$password.$user['salt']);
        if ($password != $user['password']) {
            $this->ajaxReturn(array(
                'success' => false,
                'message' => '密码错误'
            ));
        }

        // 写入会话
        session('uid', $user['id']);
        session('uname', $user['nickname']);
        session('uroles', $user['roles']);
        session('uavatar', $user['avatar']);
        
        cookie('uid', $user['id'], 3600*24*7);

        // 是否记住登录
        if ($remember_me) {
            $remember_me = md5($password.time());
            cookie('remember_me', $remember_me, 3600*24*7);
            $userModel->where("id={$user['id']}")->setField('loginSessionId', $remember_me);
        }

        // 记录日志
        saveLog($user['id'], 'login', 'login', "用户{$username}在".date('Y-m-d H:i:s')."登录系统");

        $this->ajaxReturn(array(
            'success' => true,
            'message' => '验证通过',
            'goto' => $goto
        ));

    }

    public function logout()
    {
        $goto = I('request.goto', '/Admin/Login/index');

        session('uid', null);
        cookie('uid', null);

        $this->redirect($goto);
    }

    /**
     * 检测用户是否登录
     *
     * @return bool :true,已经登录;false,未登录
     */
    private function checkLogin()
    {
        // 获取服务器端的uid
        $uid = $_SESSION['uid'];

        if ($uid == null) {
            return false;
        }

        // 获取客户端的userid
        $userid = cookie('uid');
        if ($userid == $uid) {
            return $uid;
        } else {
            return false;
        }
    }
}

源码地址:https://github.com/liebertLEOS/edu

希望本篇文章可以给新手一些指导,如有错误或不准确的地方,请在文末留言!

你可能感兴趣的:(记住登录的内部机制)