详细解读PHP的Yii框架中登陆功能的实现


Posted in PHP onAugust 21, 2015

Yii的登陆机制

Yii 生成应用时已经提供了最基础的用户登陆机制。我们用 Yii 生成一个新的应用,进入 protected/components 目录,我们可以看到 UserIdentity.php 文件,里面的 UserIdentity 类里面只有一个 public 函数如下:

public function authenticate() 
{ 
  $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,如果所以如果你的用户很有限的话,可以直接在这里面修改添加用户就行,多的话我们后面再说。函数下面的 if else 分别是用于检查用户名和密码是否有效,出错的时候生成 ERROR_USERNAME_INVALID,ERROR_PASSWORD_INVALID 这些错误。总的来说,这里进行了真正的用户名密码验证,并进行登陆后的基本逻辑处理。
     单看这个类还是看不出登陆控制流程的。遵循 Model/ Control/ View 的原则,我们可以看到登陆流程在这三方面的体现。首先进入 Models 文件夹,你可以看到一个 LoginForm 的类文件,这个类继承了 CFormModel ,为表单模型的派生类,封装了关于登陆的数据及业务逻辑。比较核心的函数如下:

/** 
 * Authenticates the password. 
 * This is the 'authenticate' validator as declared in rules(). 
 */ 
public function authenticate($attribute,$params) 
{ 
  $this->_identity=new UserIdentity($this->username,$this->password); 
  if(!$this->_identity->authenticate()) 
    $this->addError('password','用户名或密码错误'); 
} 
 
/** 
 * Logs in the user using the given username and password in the model. 
 * @return boolean whether login is successful 
 */ 
public function login() 
{ 
  if($this->_identity===null) 
  { 
    $this->_identity=new UserIdentity($this->username,$this->password); 
    $this->_identity->authenticate(); 
  } 
  if($this->_identity->errorCode===UserIdentity::ERROR_NONE) 
  { 
    $duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days 
    Yii::app()->user->login($this->_identity,$duration); 
    return true; 
  } 
  else 
    return false; 
}

    这里的 authenticate 利用 UserIdentity 类对用户名密码进行验证,而 login 函数通过检测用户身份是否已经设置及错误码是否为空,最后进行 Yii 提供的 login 函数进行登陆。$duration 可以设置身份的有效期。
    再看 Control,在 siteControler 里面有一个 action 是关于登录的,就是 actionLogin, 函数如下:

/** 
 * Displays the login page 
 */ 
public function actionLogin() 
{ 
  if (!defined('CRYPT_BLOWFISH')||!CRYPT_BLOWFISH) 
    throw new CHttpException(500,"This application requires that PHP was compiled with Blowfish support for crypt()."); 
 
  $model=new LoginForm; 
 
  // if it is ajax validation request 
  if(isset($_POST['ajax']) && $_POST['ajax']==='login-form') 
  { 
    echo CActiveForm::validate($model); 
    Yii::app()->end(); 
  } 
 
  // collect user input data 
  if(isset($_POST['LoginForm'])) 
  { 
    $model->attributes=$_POST['LoginForm']; 
    // validate user input and redirect to the previous page if valid 
    if($model->validate() && $model->login()) 
      $this->redirect(Yii::app()->user->returnUrl); 
  } 
  // display the login form 
  $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 的表:

drop table if exists `admin`; 
create table `admin` ( 
  `admin_id` int unsigned not null auto_increment comment '主键', 
  `username` varchar(32) not null comment '登录名', 
  `psw` char(40) not null comment '登录密码(两次sha1)', 
  `nick` varchar(64) not null comment '昵称', 
  `add_time` datetime not null comment '创建时间', 
  `login_time` datetime null comment '最近登录时间', 
  unique key(`username`), 
  primary key (`admin_id`) 
) engine=innodb default charset=utf8 comment='管理员表';

    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-d H:i:s'); 是进行更新用户登陆时间,并通过下一句的 save 保存到数据库中。

public function authenticate() 
{ 
  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( !($user instanceof Admin) || ($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-d H: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 登录功能

public function login($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' logged in from $ip with duration $duration.", __METHOD__);
      $this->afterLogin($identity, false, $duration);
    }

    return !$this->getIsGuest();
  }

在这里,就是简单的登录,然后执行switchIdentity方法,设置认证信息。

2、switchIdentity设置认证信息

public function switchIdentity($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 ($identity instanceof IdentityInterface) {
      $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(new Cookie($this->identityCookie));
    }
  }

这个方法比较重要,在退出的时候也需要调用这个方法。

这个方法主要有三个功能
设置session的有效期
如果cookie的有效期大于0并且允许自动登录,那么就把用户的认证信息保存到cookie中
如果允许自动登录,删除cookie信息。这个是用于退出的时候调用的。退出的时候传递进来的$identity为null

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

存储在cookie中的用户信息包含有三个值:

  1. $identity->getId()
  2. $identity->getAuthKey()
  3. $duration

getId()和getAuthKey()是在IdentityInterface接口中的。我们也知道在设置User组件的时候,这个User Model是必须要实现IdentityInterface接口的。所以,可以在User Model中得到前两个值,第三值就是cookie的有效期。

二、自动从cookie登录

从上面我们知道用户的认证信息已经存储到cookie中了,那么下次的时候直接从cookie里面取信息然后设置就可以了。

1、AccessControl用户访问控制

Yii提供了AccessControl来判断用户是否登录,有了这个就不需要在每一个action里面再判断了

  

public function behaviors()
  {
    return [
      'access' => [
        'class' => AccessControl::className(),
        'only' => ['logout'],
        'rules' => [
          [
            'actions' => ['logout'],
            'allow' => true,
            'roles' => ['@'],
          ],
        ],
      ],
    ];
  }

2、getIsGuest、getIdentity判断是否认证用户

isGuest是自动登录过程中最重要的属性。
在上面的AccessControl访问控制里面通过IsGuest属性来判断是否是认证用户,然后在getIsGuest方法里面是调用getIdentity来获取用户信息,如果不为空就说明是认证用户,否则就是游客(未登录)。

  

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

    return $this->_identity;
  }

3、renewAuthStatus 重新生成用户认证信息

   

protected function renewAuthStatus()
  {
    $session = Yii::$app->getSession();
    $id = $session->getHasSessionId() || $session->getIsActive() ? $session->get($this->idParam) : null;

    if ($id === null) {
      $identity = null;
    } else {
      /** @var IdentityInterface $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

   

protected function loginByCookie()
  {
    $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;
        /** @var IdentityInterface $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' logged in from $ip via cookie.", __METHOD__);
            $this->afterLogin($identity, true, $duration);
          }
        } elseif ($identity !== null) {
          Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
        }
      }
    }
  }

先读取cookie值,然后$data = json_decode($value, true);反序列化为数组。

这个从上面的代码可以知道要想实现自动登录,这三个值都必须有值。另外,在User Model中还必须要实现findIdentity、validateAuthKey这两个方法。

登录完成后,还可以再重新设置cookie的有效期,这样便能一起有效下去了。

$this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);

三、退出 logout

   

public function logout($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' logged out from $ip.", __METHOD__);
      if ($destroySession) {
        Yii::$app->getSession()->destroy();
      }
      $this->afterLogout($identity);
    }

    return $this->getIsGuest();
  }


  public function switchIdentity($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 ($identity instanceof IdentityInterface) {
      $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(new Cookie($this->identityCookie));
    }
  }

退出的时候先把当前的认证设置为null,然后再判断如果是自动登录功能则再删除相关的cookie信息。

PHP 相关文章推荐
用Flash图形化数据(二)
Oct 09 PHP
php 中include()与require()的对比
Oct 09 PHP
php中的时间显示
Jan 18 PHP
IP138 IP地址查询小偷实现代码
Feb 15 PHP
php入门学习知识点八 PHP中for循环基本应用之九九乘法口绝表
Jul 14 PHP
php通过COM类调用组件的实现代码
Jan 11 PHP
采集邮箱的php代码(抓取网页中的邮箱地址)
Jul 17 PHP
PHP数组及条件,循环语句学习
Nov 11 PHP
PHP代码审核的详细介绍
Jun 13 PHP
如何把php5.3版本升级到php5.4或者php5.5
Jul 31 PHP
thinkPHP显示不出验证码的原因与解决方法分析
May 20 PHP
PDO::errorCode讲解
Jan 28 PHP
使用PHP进行微信公众平台开发的示例
Aug 21 #PHP
PHP的Yii框架的基本使用示例
Aug 21 #PHP
PHP的Yii框架使用中的一些错误解决方法与建议
Aug 21 #PHP
win7系统配置php+Apache+mysql环境的方法
Aug 21 #PHP
php生成图片验证码-附五种验证码
Aug 19 #PHP
以实例全面讲解PHP中多进程编程的相关函数的使用
Aug 18 #PHP
深入探究PHP的多进程编程方法
Aug 18 #PHP
You might like
php实现分页工具类分享
2014/01/09 PHP
php检测用户是否用手机(Mobile)访问网站的类
2014/01/09 PHP
php获取域名的google收录示例
2014/03/24 PHP
PHP通过插入mysql数据来实现多机互锁实例
2014/11/05 PHP
在多个页面使用同一个HTML片段《续》
2011/03/04 Javascript
23个超流行的jQuery相册插件整理分享
2011/04/25 Javascript
这些年、我收集的JQuery代码小结
2012/08/01 Javascript
jQuery实现瀑布流布局
2014/12/12 Javascript
js实现卡片式项目管理界面UI设计效果
2015/12/08 Javascript
JS实时弹出新消息提示框并有提示音响起的实现代码
2016/04/20 Javascript
jQuery实现的placeholder效果完整实例
2016/08/02 Javascript
JS+CSS3制作炫酷的弹窗效果
2016/11/08 Javascript
微信小程序如何获知用户运行小程序的场景教程
2017/05/17 Javascript
认识jQuery的Promise的具体使用方法
2017/10/10 jQuery
深入剖析Node.js cluster模块
2018/05/23 Javascript
如何用JavaScript实现功能齐全的单链表详解
2019/02/11 Javascript
浅析vue cli3 封装Svgicon组件正确姿势(推荐)
2020/04/27 Javascript
详解element-ui动态限定的日期范围选择器代码片段
2020/07/03 Javascript
vue 项目引入echarts 添加点击事件操作
2020/09/09 Javascript
python+pyqt实现右下角弹出框
2017/10/26 Python
Python实现矩阵转置的方法分析
2017/11/24 Python
Python单元测试unittest的具体使用示例
2018/12/17 Python
浅谈cv2.imread()和keras.preprocessing中的image.load_img()区别
2020/06/12 Python
python中的django是做什么的
2020/07/31 Python
美国名牌太阳镜折扣网站:Eyedictive
2017/05/15 全球购物
玉兰油美国官网:OLAY美国
2018/10/25 全球购物
俄罗斯外国汽车和国产汽车配件网上商店:Движком
2020/04/19 全球购物
介绍一下write命令
2014/08/10 面试题
领班岗位职责范文
2014/02/06 职场文书
浪漫婚礼主题活动策划方案
2014/09/15 职场文书
教师遵守党的政治纪律情况对照检查材料
2014/09/26 职场文书
夫妻双方自愿离婚协议书怎么写
2014/12/01 职场文书
初中生物教学随笔
2015/08/15 职场文书
个人销售励志奋斗口号
2019/12/05 职场文书
Pytest allure 命令行参数的使用
2021/04/18 Python
html,css,javascript是怎样变成页面的
2023/05/07 HTML / CSS