详细解读PHP的Yii框架中登陆功能的实现
Yii的登陆机制
Yii生成应用时已经提供了最基础的用户登陆机制。我们用Yii生成一个新的应用,进入protected/components目录,我们可以看到UserIdentity.php文件,里面的UserIdentity类里面只有一个public函数如下:
publicfunctionauthenticate() { $users=array( //username=>password 'demo'=>'demo', 'admin'=>'admin', ); if(!isset($users[$this->username])) $this->errorCode=self::ERROR_USERNAME_INVALID; elseif($users[$this->username]!==$this->password) $this->errorCode=self::ERROR_PASSWORD_INVALID; else $this->errorCode=self::ERROR_NONE; return!$this->errorCode; }
这个类在components里面,会在应用一开始的时候就加载,用于最基础的用户验证,可以看到,该函数一开始只是简单地定义了两个用户demo和admin,而密码也只是demo和admin,如果所以如果你的用户很有限的话,可以直接在这里面修改添加用户就行,多的话我们后面再说。函数下面的ifelse分别是用于检查用户名和密码是否有效,出错的时候生成ERROR_USERNAME_INVALID,ERROR_PASSWORD_INVALID这些错误。总的来说,这里进行了真正的用户名密码验证,并进行登陆后的基本逻辑处理。
单看这个类还是看不出登陆控制流程的。遵循Model/Control/View的原则,我们可以看到登陆流程在这三方面的体现。首先进入Models文件夹,你可以看到一个LoginForm的类文件,这个类继承了CFormModel,为表单模型的派生类,封装了关于登陆的数据及业务逻辑。比较核心的函数如下:
/** *Authenticatesthepassword. *Thisisthe'authenticate'validatorasdeclaredinrules(). */ publicfunctionauthenticate($attribute,$params) { $this->_identity=newUserIdentity($this->username,$this->password); if(!$this->_identity->authenticate()) $this->addError('password','用户名或密码错误'); } /** *Logsintheuserusingthegivenusernameandpasswordinthemodel. *@returnbooleanwhetherloginissuccessful */ publicfunctionlogin() { if($this->_identity===null) { $this->_identity=newUserIdentity($this->username,$this->password); $this->_identity->authenticate(); } if($this->_identity->errorCode===UserIdentity::ERROR_NONE) { $duration=$this->rememberMe?3600*24*30:0;//30days Yii::app()->user->login($this->_identity,$duration); returntrue; } else returnfalse; }
这里的authenticate利用UserIdentity类对用户名密码进行验证,而login函数通过检测用户身份是否已经设置及错误码是否为空,最后进行Yii提供的login函数进行登陆。$duration可以设置身份的有效期。
再看Control,在siteControler里面有一个action是关于登录的,就是actionLogin,函数如下:
/** *Displaystheloginpage */ publicfunctionactionLogin() { if(!defined('CRYPT_BLOWFISH')||!CRYPT_BLOWFISH) thrownewCHttpException(500,"ThisapplicationrequiresthatPHPwascompiledwithBlowfishsupportforcrypt()."); $model=newLoginForm; //ifitisajaxvalidationrequest if(isset($_POST['ajax'])&&$_POST['ajax']==='login-form') { echoCActiveForm::validate($model); Yii::app()->end(); } //collectuserinputdata if(isset($_POST['LoginForm'])) { $model->attributes=$_POST['LoginForm']; //validateuserinputandredirecttothepreviouspageifvalid if($model->validate()&&$model->login()) $this->redirect(Yii::app()->user->returnUrl); } //displaytheloginform $this->render('login',array('model'=>$model)); }
该login的action是基于LoginForm将POST的表单进行验证登陆或者渲染一个新的登录页面。
最后,view的文件是site文件夹的login.php,这就是你所看到的登陆界面了。
梳理一下,我们可以清楚地看到Yii的用户登陆逻辑处理,当你在login界面输入用户名密码之后,表单将数据POST到site/login的动作,loign实例化了一个LoginForm表单模型,并根据model里面的validate函数和login函数进行登陆检测,validate会根据rule的规则验证表单数据,其中password的验证需要authenticate函数,而authenticate和login函数的验证都是基于UserIdentity的authenticate函数。所以,如果我们更改登录的逻辑,LgoinForm和loginaction都可以不用修改,直接改UserIdentity的authenticate函数就基本可以了。
以上的分析是Yii自动生成的关于用户登陆的逻辑处理代码,看起来已经很像样了不是吗?但我们的系统一般要支持很多用户访问,在代码里简单地罗列用户名和密码明显是不理智的,更为成熟的当然是请数据库来帮我们管理。假设我们在自己的数据库里面按下面的Mysql语句创建一个admin的表:
droptableifexists`admin`; createtable`admin`( `admin_id`intunsignednotnullauto_incrementcomment'主键', `username`varchar(32)notnullcomment'登录名', `psw`char(40)notnullcomment'登录密码(两次sha1)', `nick`varchar(64)notnullcomment'昵称', `add_time`datetimenotnullcomment'创建时间', `login_time`datetimenullcomment'最近登录时间', uniquekey(`username`), primarykey(`admin_id`) )engine=innodbdefaultcharset=utf8comment='管理员表';
Mysql建表完成后我们就用gii生成admin的Model,然后我们可以回到我们最初Component里面的UserIdentity.php重写authenticate函数来实现我们自己的用户名密码验证。为了安全起见,密码采用两次sha1加密,所以将采集到的密码两次sha1加密,然后在我们创建的Admin里面查找是否存在与表单输入的username对应的用户,然后比对加密过的密码,如果都通过后就可以把这个用户的常用信息由setState函数设置为Yii的user的用户字段,比如$this->setState('nick',$user->nick);这一句之后,以后可以直接通过Yii:app()->user->nick来访问当前登陆用户的昵称,而不用去查询数据库。而$user->login_time=date('Y-m-dH:i:s');是进行更新用户登陆时间,并通过下一句的save保存到数据库中。
publicfunctionauthenticate() { if(strlen($this->password)>0) $this->password=sha1(sha1($this->password)); $user=Admin::model()->findByAttributes(array('username'=>$this->username)); if($user==null) $this->errorCode=self::ERROR_USERNAME_INVALID; elseif(!($userinstanceofAdmin)||($user->psw!=$this->password)) $this->errorCode=self::ERROR_PASSWORD_INVALID; else { $this->setState('admin_id',$user->admin_id); $this->setState('nick',$user->nick); $this->setState('username',$user->username); $user->login_time=date('Y-m-dH:i:s'); $user->save(); $this->errorCode=self::ERROR_NONE; } return!$this->errorCode; }
而如果你想要修改登陆的界面,那就进入view里面site文件夹中的login.php,尽情地折腾让它变成你想要的样子,这样我们自己的登陆流程也完成了。有了Yii是不是方便极了~
设置自动登陆
自动登录的原理很简单。主要就是利用cookie来实现的
在第一次登录的时候,如果登录成功并且选中了下次自动登录,那么就会把用户的认证信息保存到cookie中,cookie的有效期为1年或者几个月。
在下次登录的时候先判断cookie中是否存储了用户的信息,如果有则用cookie中存储的用户信息来登录,
配置User组件
首先在配置文件的components中设置user组件
'user'=>[ 'identityClass'=>'app\models\User', 'enableAutoLogin'=>true, ],
我们看到enableAutoLogin就是用来判断是否要启用自动登录功能,这个和界面上的下次自动登录无关。
只有在enableAutoLogin为true的情况下,如果选择了下次自动登录,那么就会把用户信息存储起来放到cookie中并设置cookie的有效期为3600*24*30秒,以用于下次登录
现在我们来看看Yii中是怎样实现的。
一、第一次登录存cookie
1、login登录功能
publicfunctionlogin($identity,$duration=0) { if($this->beforeLogin($identity,false,$duration)){ $this->switchIdentity($identity,$duration); $id=$identity->getId(); $ip=Yii::$app->getRequest()->getUserIP(); Yii::info("User'$id'loggedinfrom$ipwithduration$duration.",__METHOD__); $this->afterLogin($identity,false,$duration); } return!$this->getIsGuest(); }
在这里,就是简单的登录,然后执行switchIdentity方法,设置认证信息。
2、switchIdentity设置认证信息
publicfunctionswitchIdentity($identity,$duration=0) { $session=Yii::$app->getSession(); if(!YII_ENV_TEST){ $session->regenerateID(true); } $this->setIdentity($identity); $session->remove($this->idParam); $session->remove($this->authTimeoutParam); if($identityinstanceofIdentityInterface){ $session->set($this->idParam,$identity->getId()); if($this->authTimeout!==null){ $session->set($this->authTimeoutParam,time()+$this->authTimeout); } if($duration>0&&$this->enableAutoLogin){ $this->sendIdentityCookie($identity,$duration); } }elseif($this->enableAutoLogin){ Yii::$app->getResponse()->getCookies()->remove(newCookie($this->identityCookie)); } }
这个方法比较重要,在退出的时候也需要调用这个方法。
这个方法主要有三个功能
设置session的有效期
如果cookie的有效期大于0并且允许自动登录,那么就把用户的认证信息保存到cookie中
如果允许自动登录,删除cookie信息。这个是用于退出的时候调用的。退出的时候传递进来的$identity为null
protectedfunctionsendIdentityCookie($identity,$duration) { $cookie=newCookie($this->identityCookie); $cookie->value=json_encode([ $identity->getId(), $identity->getAuthKey(), $duration, ]); $cookie->expire=time()+$duration; Yii::$app->getResponse()->getCookies()->add($cookie); }
存储在cookie中的用户信息包含有三个值:
- $identity->getId()
- $identity->getAuthKey()
- $duration
getId()和getAuthKey()是在IdentityInterface接口中的。我们也知道在设置User组件的时候,这个UserModel是必须要实现IdentityInterface接口的。所以,可以在UserModel中得到前两个值,第三值就是cookie的有效期。
二、自动从cookie登录
从上面我们知道用户的认证信息已经存储到cookie中了,那么下次的时候直接从cookie里面取信息然后设置就可以了。
1、AccessControl用户访问控制
Yii提供了AccessControl来判断用户是否登录,有了这个就不需要在每一个action里面再判断了
publicfunctionbehaviors() { return[ 'access'=>[ 'class'=>AccessControl::className(), 'only'=>['logout'], 'rules'=>[ [ 'actions'=>['logout'], 'allow'=>true, 'roles'=>['@'], ], ], ], ]; }
2、getIsGuest、getIdentity判断是否认证用户
isGuest是自动登录过程中最重要的属性。
在上面的AccessControl访问控制里面通过IsGuest属性来判断是否是认证用户,然后在getIsGuest方法里面是调用getIdentity来获取用户信息,如果不为空就说明是认证用户,否则就是游客(未登录)。
publicfunctiongetIsGuest($checkSession=true) { return$this->getIdentity($checkSession)===null; } publicfunctiongetIdentity($checkSession=true) { if($this->_identity===false){ if($checkSession){ $this->renewAuthStatus(); }else{ returnnull; } } return$this->_identity; }
3、renewAuthStatus重新生成用户认证信息
protectedfunctionrenewAuthStatus() { $session=Yii::$app->getSession(); $id=$session->getHasSessionId()||$session->getIsActive()?$session->get($this->idParam):null; if($id===null){ $identity=null; }else{ /**@varIdentityInterface$class*/ $class=$this->identityClass; $identity=$class::findIdentity($id); } $this->setIdentity($identity); if($this->authTimeout!==null&&$identity!==null){ $expire=$session->get($this->authTimeoutParam); if($expire!==null&&$expire<time()){ $this->logout(false); }else{ $session->set($this->authTimeoutParam,time()+$this->authTimeout); } } if($this->enableAutoLogin){ if($this->getIsGuest()){ $this->loginByCookie(); }elseif($this->autoRenewCookie){ $this->renewIdentityCookie(); } } }
这一部分先通过session来判断用户,因为用户登录后就已经存在于session中了。然后再判断如果是自动登录,那么就通过cookie信息来登录。
4、通过保存的Cookie信息来登录loginByCookie
protectedfunctionloginByCookie() { $name=$this->identityCookie['name']; $value=Yii::$app->getRequest()->getCookies()->getValue($name); if($value!==null){ $data=json_decode($value,true); if(count($data)===3&&isset($data[0],$data[1],$data[2])){ list($id,$authKey,$duration)=$data; /**@varIdentityInterface$class*/ $class=$this->identityClass; $identity=$class::findIdentity($id); if($identity!==null&&$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'loggedinfrom$ipviacookie.",__METHOD__); $this->afterLogin($identity,true,$duration); } }elseif($identity!==null){ Yii::warning("Invalidauthkeyattemptedforuser'$id':$authKey",__METHOD__); } } } }
先读取cookie值,然后$data=json_decode($value,true);反序列化为数组。
这个从上面的代码可以知道要想实现自动登录,这三个值都必须有值。另外,在UserModel中还必须要实现findIdentity、validateAuthKey这两个方法。
登录完成后,还可以再重新设置cookie的有效期,这样便能一起有效下去了。
$this->switchIdentity($identity,$this->autoRenewCookie?$duration:0);
三、退出logout
publicfunctionlogout($destroySession=true) { $identity=$this->getIdentity(); if($identity!==null&&$this->beforeLogout($identity)){ $this->switchIdentity(null); $id=$identity->getId(); $ip=Yii::$app->getRequest()->getUserIP(); Yii::info("User'$id'loggedoutfrom$ip.",__METHOD__); if($destroySession){ Yii::$app->getSession()->destroy(); } $this->afterLogout($identity); } return$this->getIsGuest(); } publicfunctionswitchIdentity($identity,$duration=0) { $session=Yii::$app->getSession(); if(!YII_ENV_TEST){ $session->regenerateID(true); } $this->setIdentity($identity); $session->remove($this->idParam); $session->remove($this->authTimeoutParam); if($identityinstanceofIdentityInterface){ $session->set($this->idParam,$identity->getId()); if($this->authTimeout!==null){ $session->set($this->authTimeoutParam,time()+$this->authTimeout); } if($duration>0&&$this->enableAutoLogin){ $this->sendIdentityCookie($identity,$duration); } }elseif($this->enableAutoLogin){ Yii::$app->getResponse()->getCookies()->remove(newCookie($this->identityCookie)); } }
退出的时候先把当前的认证设置为null,然后再判断如果是自动登录功能则再删除相关的cookie信息。