详细解读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信息。