zoukankan      html  css  js  c++  java
  • 杂集一(yii自动登陆过程浅析)

    最近有个系统使用yii开发的,里面有不少细节上的调整。

    一、yii自动登陆

    yii自动生成的骨架中有一个登陆功能,勾选后,系统会保存部分认证信息,以后再登陆是系统会读取这些认证信息,这样就直接跳过了登陆的过程。

    我们的系统中保留了这部分功能,但是使用过程中发现有部分信息没有保存,所以登陆成功后会出现很多问题,所以我仔细的看了登陆的流程。

    请求login方法后

     1 public function login()
     2     {
     3         if($this->_identity===null)
     4         {
     5             $this->_identity=new UserIdentity($this->username,$this->password);
     6             $this->_identity->authenticate();
     7         }
     8         if($this->_identity->errorCode===UserIdentity::ERROR_NONE)
     9         {
    10             $duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
    11             Yii::app()->user->login($this->_identity,$duration);
    12             return true;
    13         }
    14         else
    15             return false;
    16     }
    View Code

    系统首先发现还没有创建认证标识 _identity,所以,系统首先会建立UserIdentity的实例化对象,然后调用该对象的authenticate方法。

     1 public function authenticate()
     2     {
     3         $username = strtolower($this->username);
     4         $user = User::model()->find('LOWER(username)=?', array($username));
     5         if($user === null){
     6             $this->errorCode = self::ERROR_USERNAME_INVALID;
     7         }elseif (!$user->validatePassword($this->password)) {
     8             $this->errorCode = self::ERROR_PASSWORD_INVALID;
     9         }else{
    10             $this->_id = $user->id;
    11             $this->username = $user->username;
    12             $this->errorCode = self::ERROR_NONE;
    13         }
    14         return $this->errorCode;
    15     }

    这个过程其实比较简单易懂,就是拿用户名去数据库检索,然后匹配密码,根据匹配结果返回不同的认证结果。当然这边需要用户名作为唯一认证,如果不是需要拿用户表主键。

    $duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
    Yii::app()->user->login($this->_identity,$duration);

    认证成功后,系统开始执行实际的登陆过程。Yii::app()->user实例化出一个cwebuser对象,然后调用这个对象中的login方法

     1 public function login($identity,$duration=0)
     2 {
     3     $id=$identity->getId();
     4     $states=$identity->getPersistentStates();
     5     if($this->beforeLogin($id,$states,false))
     6     {
     7         $this->changeIdentity($id,$identity->getName(),$states);
     8 
     9         if($duration>0)
    10         {
    11             if($this->allowAutoLogin)
    12                 $this->saveToCookie($duration);
    13             else
    14                 throw new CException(Yii::t('yii','{class}.allowAutoLogin must be set true in order to use cookie-based authentication.',
    15                     array('{class}'=>get_class($this))));
    16         }
    17 
    18         $this->afterLogin(false);
    19     }
    20     return !$this->getIsGuest();
    21 } 

    首先获取当前登陆用户的id标识,然后调用$identity->getPersistentStates()获取认证对象的返回需要持久化的身份状态。调用CWebUser的beforelogin方法

    1 protected function beforeLogin($id,$states,$fromCookie)
    2 {
    3     return true;
    4 } 
    View Code

    然后将id,用户名和之前获取的持久化认证身份状态一起存入session中

    1 protected function changeIdentity($id,$name,$states)
    2 {
    3     Yii::app()->getSession()->regenerateID();
    4     $this->setId($id);
    5     $this->setName($name);
    6     $this->loadIdentityStates($states);
    7 } 

    这样就实现了登陆的流程。然后上面如果勾选了自动登陆则定义cookie存储的默认时间为30天,发现存储的时间戳不为0时,开始自动登陆流程的操作,当配置文件中同样配置了允许自动登陆时,将认证信息保存到cookie中,否则抛出异常。所以当抛出allowAutoLogin must be set true in order to use cookie-based authentication.这样的异常页面时,需要将配置文件中allowAutoLogin打开。

    yii在将认证信息保存到cookie时做了一些操作。

     1 protected function saveToCookie($duration)
     2 {
     3     $app=Yii::app();
     4     $cookie=$this->createIdentityCookie($this->getStateKeyPrefix());
     5     $cookie->expire=time()+$duration;
     6     $data=array(
     7         $this->getId(),
     8         $this->getName(),
     9         $duration,
    10         $this->saveIdentityStates(),
    11     );
    12     $cookie->value=$app->getSecurityManager()->hashData(serialize($data));
    13     $app->getRequest()->getCookies()->add($cookie->name,$cookie);
    14 } 
     1 protected function createIdentityCookie($name)
     2 {
     3     $cookie=new CHttpCookie($name,'');
     4     if(is_array($this->identityCookie))
     5     {
     6         foreach($this->identityCookie as $name=>$value)
     7             $cookie->$name=$value;
     8     }
     9     return $cookie;
    10 } 

    利用认证前缀名创建一个cookie,然后获取id,name和持久化认证身份状态信息,经过序列化之后一系列操作流程将这些信息存放到cookie中。

    保存cookie的过程。首先实例化一个安全管理器组件,然后挑用这个安全管理器组件的hashData()

    public function hashData($data,$key=null)
    {
        return $this->computeHMAC($data,$key).$data;
    } 

    这里调用了computerHAMC()用来生成HMAC的私钥。如果没有明确指定密钥,那么会生成和使用随机密钥。

     1 protected function computeHMAC($data,$key=null)
     2 {
     3     if($key===null)
     4         $key=$this->getValidationKey();
     5 
     6     if(function_exists('hash_hmac'))
     7         return hash_hmac($this->hashAlgorithm, $data, $key);
     8 
     9     if(!strcasecmp($this->hashAlgorithm,'sha1'))
    10     {
    11         $pack='H40';
    12         $func='sha1';
    13     }
    14     else
    15     {
    16         $pack='H32';
    17         $func='md5';
    18     }
    19     if($this->strlen($key) > 64)
    20         $key=pack($pack, $func($key));
    21     if($this->strlen($key) < 64)
    22         $key=str_pad($key, 64, chr(0));
    23     $key=$this->substr($key,0,64);
    24     return $func((str_repeat(chr(0x5C), 64) ^ $key) . pack($pack, $func((str_repeat(chr(0x36), 64) ^ $key) . $data)));
    25 } 
     1 public function getValidationKey()
     2 {
     3     if($this->_validationKey!==null)
     4         return $this->_validationKey;
     5     else
     6     {
     7         if(($key=Yii::app()->getGlobalState(self::STATE_VALIDATION_KEY))!==null)
     8             $this->setValidationKey($key);
     9         else
    10         {
    11             $key=$this->generateRandomKey();
    12             $this->setValidationKey($key);
    13             Yii::app()->setGlobalState(self::STATE_VALIDATION_KEY,$key);
    14         }
    15         return $this->_validationKey;
    16     }
    17 }

     CApplication->getGlobalState()这边再往下越来越深,基本超越我目前的接受范围,就不深入挖掘了。不过$this->detachEventHandler('onEndRequest',array($this,'saveGlobalState'));这句话让我有了不小的感触,但是还不能那么准确的说出这是什么样的感觉。这感觉就好像是我编程过程中一直缺失的部分。这段我感觉就是生成一个全局范围的状态数组。至于数组的内容就是从持久存储加载状态数据。如果没有获取到则建立一个空的全局状态数组。

    1 public function getGlobalState($key,$defaultValue=null)
    2 {
    3     if($this->_globalState===null)
    4         $this->loadGlobalState();
    5     if(isset($this->_globalState[$key]))
    6         return $this->_globalState[$key];
    7     else
    8         return $defaultValue;
    9 } 
    View Code

     如果上面返回持久化存储状态信息失败为空,就随机生成一个新的key

    1 protected function generateRandomKey()
    2 {
    3     return sprintf('%08x%08x%08x%08x',mt_rand(),mt_rand(),mt_rand(),mt_rand());
    4 } 

    再将这个生成的key存储到持久化存储信息中。key生成后需要将key写入认证key中

    1 public function setValidationKey($value)
    2 {
    3     if(!empty($value))
    4         $this->_validationKey=$value;
    5     else
    6         throw new CException(Yii::t('yii','CSecurityManager.validationKey cannot be empty.'));
    7 } 

    key生成后,如果系统存在hash_hmac方法,就调用hash_hmac生成一个hash值。如果系统不支持hash_hmac函数,则判断当前使用的加密方式,分别为sha1和md5,然后根据不同情况对key进行处理,最终得到一个64位的串。然后再得到加密串。(其实我已经混乱了。。。)然后将这个加密串写入cookie就可以了。(本来我还觉得能拿到加密串反向或许可以推出密码,现在看来是不可逆并且逆向过程也会死人-_-)

    完成存储cookie过程后,页面接下来跳转到来源页。登陆过程至此完成。

     退出过程相较前面的登陆,工作就少得多了。毕竟摧毁起来比建立要方便的多。

     1 public function logout($destroySession=true)
     2 {
     3     if($this->beforeLogout())
     4     {
     5         if($this->allowAutoLogin)
     6         {
     7             Yii::app()->getRequest()->getCookies()->remove($this->getStateKeyPrefix());
     8             if($this->identityCookie!==null)
     9             {
    10                 $cookie=$this->createIdentityCookie($this->getStateKeyPrefix());
    11                 $cookie->value=null;
    12                 $cookie->expire=0;
    13                 Yii::app()->getRequest()->getCookies()->add($cookie->name,$cookie);
    14             }
    15         }
    16         if($destroySession)
    17             Yii::app()->getSession()->destroy();
    18         else
    19             $this->clearStates();
    20         $this->afterLogout();
    21     }
    22 } 

    就是删除相关cookie和session,为防止cookie清除的不干净,还顺带重定义认证cookie的内容,存空,置为会话cookie即可。所以,如果执行了登出操作,下一次就不会自动登陆,所以,这边假设是直接关闭浏览器的。如果没有设置自动登陆,session被清除,也算登出了。

    自动登陆,但CWebUser实例化后会调用init()

     1 public function init()
     2 {
     3     parent::init();
     4     Yii::app()->getSession()->open();
     5     if($this->getIsGuest() && $this->allowAutoLogin)
     6         $this->restoreFromCookie();
     7     else if($this->autoRenewCookie && $this->allowAutoLogin)
     8         $this->renewCookie();
     9     if($this->autoUpdateFlash)
    10         $this->updateFlash();
    11 
    12     $this->updateAuthStatus();
    13 } 

    如果还没有登陆且开启了自动登录则执行restoreFromCookie()即从cookie获取登陆信息。如果不是来宾帐号且开启了自动登陆并允许更新cookie则更新cookie,我对前面解析的机制还不理解,不过通过一些调试手段知道执行了第一个if的内容

     1 protected function restoreFromCookie()
     2 {
     3     $app=Yii::app();
     4     $request=$app->getRequest();
     5     $cookie=$request->getCookies()->itemAt($this->getStateKeyPrefix());
     6     if($cookie && !empty($cookie->value) && ($data=$app->getSecurityManager()->validateData($cookie->value))!==false)
     7     {
     8         $data=@unserialize($data);
     9         if(is_array($data) && isset($data[0],$data[1],$data[2],$data[3]))
    10         {
    11             list($id,$name,$duration,$states)=$data;
    12             if($this->beforeLogin($id,$states,true))
    13             {
    14                 $this->changeIdentity($id,$name,$states);
    15                 if($this->autoRenewCookie)
    16                 {
    17                     $cookie->expire=time()+$duration;
    18                     $request->getCookies()->add($cookie->name,$cookie);
    19                 }
    20                 $this->afterLogin(true);
    21             }
    22         }
    23     }
    24 }

    这个操作就是获取cookie值,然后放进安全管理组件中进行判断是否被篡改

     1 public function validateData($data,$key=null)
     2 {
     3     $len=$this->strlen($this->computeHMAC('test'));
     4     if($this->strlen($data)>=$len)
     5     {
     6         $hmac=$this->substr($data,0,$len);
     7         $data2=$this->substr($data,$len,$this->strlen($data));
     8         return $hmac===$this->computeHMAC($data2,$key)?$data2:false;
     9     }
    10     else
    11         return false;
    12 } 

    这个过程我已经无力理解了,跳过。。。。

    cookie验证通过后,将cookie中数据反序列化并放入session中,登陆完成。

    分析完上面的内容,我发现,牛逼就是牛逼。。。。

    由于我们的代码缺少存储持续化存储的身份状态,所以反序列化后很多字段没有值,还是需要从数据库中再拉取一便,但是系统认为主要的认证信息齐全了,不需要再查询数据库了,所以就会产生一些bug。可以考虑登陆的时候调用setPersistentStates()来存储需要用到的认证信息,但是目前也不知道那些信息需要存储,每次都要添加其实并不方便,也可以登陆完成后再执行一遍查询操作,更新user信息。这样效率上有影响。所以我们最终决定改变现有流程,选择自动登陆后(名称改为记住密码)将用户名密码加密后存储到cookie中,打开登陆页面后解密后放进用户名密码栏,省去输入用户名密码的时间,实际上不会自动登陆。

    1 $duration = 0;
    2 if($this->rememberMe){
    3   setcookie('username', $this->username, time() + 60*60*24*7 );
    4   setcookie('password', $this->password, time() + 60*60*24*7 );
    5}

    加密的过程处于对客户方的尊重,代码就不展示了。就是将系统的cookie有效期置位0,即设为会话即可。

    if(isset($_COOKIE['username'])){  
      $username = $_COOKIE['ofly_username'];
      $model->username = $username;
    }

    登陆时解密逆向一下,这样就可以记录用户名,密码了。

     

  • 相关阅读:
    angular6 input节流
    遇到跨域别害怕,我来帮你搞定它
    angular4,6 引入第三方插件的方法
    带有动画的字体
    angular 多端打包
    css 常用布局
    chrome 49 版本 跨越 --args --disable-web-security --user-data-dir
    JavaScript中的document.cookie的使用
    ios/iphone手机请求微信用户头像错位BUG及解决方法
    Safari 前端开发调试 iOS 完美解决方案
  • 原文地址:https://www.cnblogs.com/listenRain/p/yii_autologin.html
Copyright © 2011-2022 走看看