张大帅 2016-08-16 09:38:52 7758次浏览 8条评论 18 11 0

Hello,大家好,大帅又来了。
今日有点忙,更新的慢了一点,今天我来分享一下Yii框架的登录机制。
Yii框架的登录机制总体来说,借助了session和cookie的机制进行登录,大家都知道,一般情况下,cookie是保存在浏览器端,每次请求中,浏览器都会把该域下的cookie发送给服务器。而session中保存了会话内容,浏览器关闭后,会话结束。如果想详细了解一下cookie和session的机制,可以参考该篇文章,http://blog.csdn.net/fangaoxin/article/details/6952954/解释的还是比较详细的。
登录模块,这里有几种场景

  1. 从未登录过系统。
  2. 登录过程。(设置cookie和session信息)
  3. 登录系统之后,浏览器未关闭。(靠session信息进行登录验证等)
  4. 关闭浏览器之后,第一次页面自动登录。(靠cookie信息进行登录验证等)
    和以前的代码分析不同,在此不讨论特别复杂的场景,系统首先验证登录的代码要数layout/main.php中了
Yii::$app->user->isGuest ? (
    ['label' => 'Login', 'url' => ['/site/login']]
) : (
    '<li>'
    . Html::beginForm(['/site/logout'], 'post')
    . Html::submitButton(
        'Logout (' . Yii::$app->user->identity->username . ')',
        ['class' => 'btn btn-link']
    )
    . Html::endForm()
    . '</li>'
))

在这段代码中,系统首先验证是不是访问,如果不是访客,则打印登录入口,如果不是访客,那么打印用户信息。无论是什么场景,系统都要先判断是不是访客。结合三种场景,我们逐个进行分析。
一、首先第一个场景,用户从来没有登录过系统。
Yii::$app->user对应的是@app/web/user.php文件,因为user.php继承与component,所以isGuest对应的其实是getIsGuest()方法,

    public function getIsGuest()
    {
        return $this->getIdentity() === null;
    }
    public function getIdentity($autoRenew = true)
    {
        if ($this->_identity === false) {
            if ($this->enableSession && $autoRenew) {
                $this->_identity = null;
                $this->renewAuthStatus();
            } else {
                return null;
            }
        }

        return $this->_identity;
    }

此中场景下$this->_identity === false是成立的, 接着进入到$this->renewAuthStatus()方法,接下来进入到该方法,注意,该方法很重要,因为它决定着session和cookie的验证。

    protected function renewAuthStatus()
    {
        $session = Yii::$app->getSession();
        $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null;
        if ($id === null) {
            $identity = null;
        } else {
            /* @var $class IdentityInterface */
            $class = $this->identityClass;
            $identity = $class::findIdentity($id);
        }

        $this->setIdentity($identity);
        if ($identity !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
            $expire = $this->authTimeout !== null ? $session->get($this->authTimeoutParam) : null;
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $session->get($this->absoluteAuthTimeoutParam) : null;
            if ($expire !== null && $expire < time() || $expireAbsolute !== null && $expireAbsolute < time()) {
                $this->logout(false);
            } elseif ($this->authTimeout !== null) {
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
            }
        }
        if ($this->enableAutoLogin) {
            if ($this->getIsGuest()) {
                $this->loginByCookie();
            } elseif ($this->autoRenewCookie) { 
                $this->renewIdentityCookie();
            }
        }
    }

首先获得session,接着获得session['__id'] (此种场景下为null), 所以$id必然为null,进而$identity=null, 接着这段代码$this->setIdentity($identity);的主要作用就是讲$this->_identity赋值。此时还是为空,接着代码直接跳转到了这段:

        if ($this->enableAutoLogin) {
            if ($this->getIsGuest()) {  //如果没有登录,那么就试着用cookie登录一下
                $this->loginByCookie();
            } elseif ($this->autoRenewCookie) { //如果已经登录,而且默认更新cookie,那么就更新cookie
                $this->renewIdentityCookie();
            }
        }

此时再去getIsGuest的时候,还是会走到getIdentity()函数,不过此时的$this->_identity已经不是false了,而是前一次设置的null了,这个时候就$this->getIsGuest()为true了,证明是游客,那么就用cookie登录一下试试,因为此时场景是从未登录过的场景,所以此时肯定是登录不上的。那么再往回返回就得到了isGuest为真了。
二、登录的场景。
登录的场景是一个比较复杂而简单的过程,因为Yii框架默认了两个账号,

    private static $users = [
        '100' => [
            'id' => '100',
            'username' => 'admin',
            'password' => 'admin',
            'authKey' => 'test100key',
            'accessToken' => '100-token',
        ],
        '101' => [
            'id' => '101',
            'username' => 'demo',
            'password' => 'demo',
            'authKey' => 'test101key',
            'accessToken' => '101-token',
        ],
    ];

如果大家想要用数据库来进行用户的操作的话,这里不做详解,但是有一点是要说明的是,重写app/models/User.php的时候,可以继承ActiveRecord,但是必须也要implents \yii\web\IdentityInterface, 否则是不会成功的,因为后面的用户验证等等,都是走的是Identity这个类型的。
登录的时候在LoginForm.php中,有这么一段话,Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);,此时调用的正是app\web\User.php中的login()方法,接着来分析一下。

    public function login(IdentityInterface $identity, $duration = 0)
    {
        if ($this->beforeLogin($identity, false, $duration)) {
            $this->switchIdentity($identity, $duration);
            $id = $identity->getId();
            $ip = Yii::$app->getRequest()->getUserIP();
            if ($this->enableSession) {
                $log = "User '$id' logged in from $ip with duration $duration.";
            } else {
                $log = "User '$id' logged in from $ip. Session not enabled.";
            }
            Yii::info($log, __METHOD__);
            $this->afterLogin($identity, false, $duration);
        }

        return !$this->getIsGuest();
    }

第一个参数就是刚才说的Identity(解释了为什么重写models\User的话为什么必须implenets \yii\web\IdentityInterface),此时的参数就是一个包含用户基本信息的一条实例。首先是这块代码,$this->switchIdentity($identity, $duration);进入到swithIdentity()

    public function switchIdentity($identity, $duration = 0)
    {
        $this->setIdentity($identity);

        if (!$this->enableSession) {
            return;
        }

        $session = Yii::$app->getSession();
        if (!YII_ENV_TEST) {
            $session->regenerateID(true);
        }
        $session->remove($this->idParam);
        $session->remove($this->authTimeoutParam);
        if ($identity) {
            $session->set($this->idParam, $identity->getId());
            if ($this->authTimeout !== null) {
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
            }
            if ($this->absoluteAuthTimeout !== null) {
                $session->set($this->absoluteAuthTimeoutParam, time() + $this->absoluteAuthTimeout);
            }
            if ($duration > 0 && $this->enableAutoLogin) {
                $this->sendIdentityCookie($identity, $duration);
            }
        } elseif ($this->enableAutoLogin) {
            Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
        }
    }

首先将$this->_identity = $identity,如果没有开启session那么直接返回了。反之获得session,(这里告诉一个秘密,如果你关闭浏览器,将该域下的所有cookie清除之后,重新打开浏览器直接进入登录页面,输入信息之后,点击登录会报错的),获得session之后,将session是该用户ID的信息给清理掉。然后重新设置该用户的session信息,并且重新设置过期时间。接着往下看

if ($duration > 0 && $this->enableAutoLogin) {
    $this->sendIdentityCookie($identity, $duration);
}

如果$duration和enableAutologin条件成立的话,那么去设置cookie信息。sendIdentityCookie()函数。

    protected function sendIdentityCookie($identity, $duration)
    {
        $cookie = new Cookie($this->identityCookie);
        $cookie->value = json_encode([
            $identity->getId(),
            $identity->getAuthKey(),
            $duration,
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        Yii::$app->getResponse()->getCookies()->add($cookie);
    }

就是讲用户的id,authkey,duration信息设置到cookie中。
重新返回到login()函数中,已经将cookie和session信息更新完了。接着最后一句话,!$this->getIsGuest();重新验证getIsGuest();此时$this->_identity已经有值了,所以算是登录成功了。
三、第三种场景是登录成功之后,跳转页面的时候(此时未关闭浏览器)
因为HTTP协议是无状态的,所以每一次访问,系统都要进行用户信息的验证。那么在未关闭浏览器的时候,我们看看系统是怎样获得用户信息的。(注意此时浏览器已经有了cookie,服务端也已经有了session信息了)
首先还是判断是不是游客。重新回到

    public function getIdentity($autoRenew = true)
    {
        if ($this->_identity === false) {
            if ($this->enableSession && $autoRenew) {
                $this->_identity = null;
                $this->renewAuthStatus();
            } else {
                return null;
            }
        }

        return $this->_identity;
    }

此时的$this->_identity仍然为false,那么顺道走到$this->renewAuthStatus();进入到renewAuthStatus()函数中。

    protected function renewAuthStatus()
    {
        $session = Yii::$app->getSession();
        $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null;
        //从来没有登录过的时候$id = null;
        //如果登录过的时候$id = $_SESSION['__id'];还不知道该值在登录时候,退出时候做了什么修改。
        if ($id === null) {
            $identity = null;
        } else {
            /* @var $class IdentityInterface */
            $class = $this->identityClass;
            $identity = $class::findIdentity($id);
        }

        $this->setIdentity($identity);
        //如果$id = null; 则Identity为null; 从来没有登录过
        //如果$id !=null; 则Identity为$class::findIdentity($id);
        if ($identity !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
            $expire = $this->authTimeout !== null ? $session->get($this->authTimeoutParam) : null;
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $session->get($this->absoluteAuthTimeoutParam) : null;
            if ($expire !== null && $expire < time() || $expireAbsolute !== null && $expireAbsolute < time()) {
                $this->logout(false);
            } elseif ($this->authTimeout !== null) {
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
            }
        }

        if ($this->enableAutoLogin) {
            if ($this->getIsGuest()) { //如果没有登录,那么就试着用cookie登录一下
                $this->loginByCookie();
            } elseif ($this->autoRenewCookie) { //如果已经登录,而且默认更新cookie,那么就更新cookie
                $this->renewIdentityCookie();
            }
        }
    }

因为这种场景是登录过,那么就说明用户过来是有cookie和session了,在这段代码中的$id,就是刚才登录的时候设置的$session['__id'],那么通过$identity = $class::findIdentity($id)就可以定位到该人,并且将$this->identity = $identity; 并在最后更新了cookie的有效时间,到此为止,就可以得到用户的基本信息。
四、第四种场景是用户关了浏览器,但是cookie还是存在的,我们继续从头开始说起。
还是按照是自动登录状态来讲,来看看系统是怎么通过cookie得到用户的信息的。
首先还是判断是不是游客,同样经过getIsGuest()函数,

    public function getIdentity($autoRenew = true)
    {
        if ($this->_identity === false) {
            if ($this->enableSession && $autoRenew) {
                $this->_identity = null;
                $this->renewAuthStatus();
            } else {
                return null;
            }
        }

        return $this->_identity;
    }

由于打开浏览器,第一次打开页面,session的信息是不存在的,所以此时的_identity肯定为false的,那么就会进入到renewAuthStatus函数,

    protected function renewAuthStatus()
    {
        $session = Yii::$app->getSession();
        $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null;
        //从来没有登录过的时候$id = null;
        //如果登录过的时候$id = $_SESSION['__id'];还不知道该值在登录时候,退出时候做了什么修改。
        if ($id === null) {
            $identity = null;
        } else {
            /* @var $class IdentityInterface */
            $class = $this->identityClass;
            $identity = $class::findIdentity($id);
        }

        $this->setIdentity($identity);
        //如果$id = null; 则Identity为null; 从来没有登录过
        //如果$id !=null; 则Identity为$class::findIdentity($id);
        if ($identity !== null && ($this->authTimeout !== null || $this->absoluteAuthTimeout !== null)) {
            $expire = $this->authTimeout !== null ? $session->get($this->authTimeoutParam) : null;
            $expireAbsolute = $this->absoluteAuthTimeout !== null ? $session->get($this->absoluteAuthTimeoutParam) : null;
            if ($expire !== null && $expire < time() || $expireAbsolute !== null && $expireAbsolute < time()) {
                $this->logout(false);
            } elseif ($this->authTimeout !== null) {
                $session->set($this->authTimeoutParam, time() + $this->authTimeout);
            }
        }

        //如果默认并没有配置enableAutoLogin, 此时代码不走这一段
        if ($this->enableAutoLogin) {
            if ($this->getIsGuest()) { //如果没有登录,那么就试着用cookie登录一下
                $this->loginByCookie();
            } elseif ($this->autoRenewCookie) { //如果已经登录,而且默认更新cookie,那么就更新cookie
                $this->renewIdentityCookie();
            }
        }
    }

在此函数中,session信息是不存在id信息的,一直到if ($this->enableAutoLogin) 这句话之前,一直是拿不到用户的基本信息的,应为$id一直为null,继续沿着enableAutologin()往下走的话,就会来到loginByCookie()函数,很容易猜测到系统是通过cookie来登录的,

    protected function loginByCookie()
    {
        $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
        if ($value === null) {
            return;
        }

        $data = json_decode($value, true);
        if (count($data) !== 3 || !isset($data[0], $data[1], $data[2])) {
            return;
        }

        list ($id, $authKey, $duration) = $data;
        /* @var $class IdentityInterface */
        $class = $this->identityClass;
        $identity = $class::findIdentity($id);
        if ($identity === null) {
            return;
        } elseif (!$identity instanceof IdentityInterface) {
            throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface.");
        }

        if ($identity->validateAuthKey($authKey)) {
            if ($this->beforeLogin($identity, true, $duration)) {
                $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
                $ip = Yii::$app->getRequest()->getUserIP();
                Yii::info("User '$id' logged in from $ip via cookie.", __METHOD__);
                $this->afterLogin($identity, true, $duration);
            }
        } else {
            Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
        }
    }

首先拿到的是$value信息,之前登录过之后,系统会设置cookie信息, public $identityCookie = ['name' => '_identity', 'httpOnly' => true]; cookie信息里面包含的是name字段为_identity,默认打出来的$value信息为["100","test100key",2592000]; 当你关闭浏览器并且以前登录过之后,第一次进入系统之后才会看到该信息,如果你刷新一下浏览器就会看不到该信息。httpOnly的作用不做赘述,主要是为了cookie安全而设。list ($id, $authKey, $duration) = $data;之后该句话,就可以从cookie信息中,拿到登录过用户的$id, $authKey, $duration,之后的登录流程还是和之前描述的类似了,通过$identity得到用户的基本信息,并更新session和cookie有效时间等。之后用户就可以拿到Yii::$app->user->identity->**信息了。

总的来说这几个过程就是这样的:
系统会通过session信息和cookie信息来判断用户的。
当用户从来没有登录过系统的时候,session系统中是没有id的,所以拿不到用户信息。
当用户登录的过程中,系统会同时设置session和cookie的id信息。
当用户登录之后,没有关闭浏览器,系统会通过session['__id']信息来识别用户,之后通过identityClass查找到该用户id的详细信息。
当用户登录之后,关闭了浏览器,也就是关闭了session会话,第一次进入系统之后,系统通过cookie信息来找到用户的id,之后再更新一下session信息和cookie信息(过期时间)。
当用户等路之后,关闭浏览器之后,重新打开一个页面上面描述了是通过cookie识别的,之后再跳转到其他的页面,系统是通过session信息来识别用户的。

登录系统就是这么简单。
之后还有其他内容更新,敬请期待。。。

觉得很赞
  • 评论于 2016-09-21 17:53 举报

    好文,好文,支持

  • 评论于 2016-09-09 09:50 举报

    你好,分析的非常详细。新手受教了~

  • 评论于 2016-09-02 10:17 举报

    好文,希望快快更新啊

  • 评论于 2016-08-22 10:41 举报

    新手顶你~

  • 评论于 2016-08-21 20:53 举报

    你好,看了此文受益匪浅,想请教个问题。 Yii::$app->user指向到user模型是怎么实现的,还有Yii::$app->db这种是用注册树模式还是其他什么实现的?万分感谢

    3 条回复
    评论于 2016-08-23 09:11 回复

    加载的是组件啊

    评论于 2016-10-08 20:20 回复

    我又把代码过了一遍,详细给你介绍下,在初始化的时候会将coreComponents里面都放在config['components']中,之后调用了Component::_construct($config),进而调用的是Object::_construct($config),进而调用Yii::configure($this,$config),在里面会做个循环,将config里面所有的字段都赋值给$app,赋值的时候会调用相应的setter,当调用到setcomponents的时候,这个函数在serviceLocator里面,会借用serviceLocator的set机制进行实例化,至此,在调用Yii::createObject,进而就调用di容器,进而实例化了。。。。漫长吧,就是这样的。

    评论于 2016-10-08 21:11 回复

    谢谢,看懂了部分,明天翻翻源码验证验证,再次感谢

  • 评论于 2016-08-17 17:55 举报

    我顶我顶我顶~

    觉得很赞
  • 评论于 2016-08-17 15:10 举报

    顶起来~~~
    顶起来~~~
    顶起来~~~

    觉得很赞
  • 评论于 2016-08-16 09:39 举报

    顶起来~~~

    , 觉得很赞
您需要登录后才可以评论。登录 | 立即注册