杂集一(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;
}

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

 

 

 

 

 

 

 

 

 

 

posted @ 2013-07-05 16:52  albafica  阅读(1599)  评论(0编辑  收藏  举报