第七章:迭代4:用户管理和权限认证
在前面很短的时间内,我们完成了大量开发工作。Trackstar应用程序基本功能的基础已经奠定。目前为止,我们已经有能力去管理该项目和其中存在的问题,而这个能力正是Trackstar应用程序需要实现的首要目标。当然,还有许多事在向我们招手。
请回顾一下第三章,当时我们介绍这一应用程序的时候,我们将它描述为基于角色的应用程序,它允许建立用户帐号,一旦用户获得授权并且通过认证就可以使用一些功能。为了让这个应用程序比单用户系统更有用,我们需要添加基于项目的用户管理能力。下述2次迭代会完成这一功能。
迭代计划
最初我们使用yiic命令行工具建立我们的Trackstar应用程序时,我们注意到(Yii)系统已经自动为我们创建了基础登录功能。当前登录页面只能使用2对帐号/密码进行登录(demo/demo和admin/admin)。你或许还记得,我们必须在登录的情况下,才可以对项目和相关问题进行CRUD操作。
这些用于认证的基础脚手架代码,确实为我们提供了一个好的开端,但是我们需要进行一定的修改,使之支持更多的用户。同时我们需要向应用程序添加用户CRUD功能,用来对多用户进行管理。这次迭代聚焦在使用User表扩展认证模型,和适当添加基本用户数据管理所需功能。
为了达到上述目标,我们需要确定所有在本次迭代将要实现的细节。以下列表中列出了这些细节:
-
- 建立一个用于存放所有我们需要功能的Controller(控制器)类:
- 建立新用户
- 从数据库获取一列已存在用户信息
- 更新/编辑已存在用户信息
- 删除已存在用户
- 建立View File(视图文件)和呈现层逻辑,用来:
- 显示用于建立新项目的表单
- 用列表显示所有已存在项目
- 显示授予某一用户权限,去编辑一个已存在项目的表单
- 为项目列表添加一个删除按钮,实现删除项目
- 调整新建用户表单,使之可以被用于外部用户自助注册
- 修改认证流程,使用数据库来认证用户登录信息
- 建立一个用于存放所有我们需要功能的Controller(控制器)类:
运行测试套件
在我们添加新功能之前运行一下测试套件绝对是一个好注意。测试套件包括之前每一次迭代,包括我们为应用添加的代码,也包括我们所添加过的测试套件。随着测试套件的增多,我们的应用程序向我们反馈其健壮程度的能力也在不断增长。在我们进行改变前,请确保一切工作如常。从测试文件包(/protected/tests/)运行一次单元测试:
% phpunit unit/ PHPUnit 3.4.12 by Sebastian Bergmann. .......... Time: 0 seconds OK (10 tests, 26 assertions)
一切都那么美好,让我们尽情的投入这次迭代。
创建用户CRUD
由于我们正在建立一个基于用户的Web应用,我们必须有添加和管理用户的方式。在第6章我们添加了表tbl_user到我们的数据库。你可能记得,我们将它作为一个练习(建立与之相关的AR模型类)留给读者。如果你还没有建立这个类,你需要现在建立它。
使用Gii代码创建工具,新建一个模型类的简要提示。通过http://localhost/trackstar/index.php?r=gii打开Gii工具并点击Model Generator连接,设置表前缀为tbl_。在Table Name输入框填入tbl_user, Model Class输入框会自动填入User。
当表单填写完成,点击Preview按钮,可以得到一个可以弹出将要生成代码情况的连接。然后点击Generate按钮来确定在/protected/models下生成一个新User.php 模型类文件。
伴随User AR 类的完成和基于以前从Gii操作中获得的经验,建立CRUD脚手架就是小菜一碟。以下作为一个简单的回顾:
-
- 打开http://localhost/trackstar/index.php?r=gii
- 在可选生成器列表中点击Crud Generator连接
- 在Model Class框输入User, Controller ID框将会自动写入user.
当上述被完成后,我们可以在http://localhost/trackstar/index.php?r=user浏览我们的用户列表。在上一次迭代中,我们手动添加了一些用户进入系统,所以接下来我们可以控制项目,项目中的问题和用户之间的关系了。所以在上述地址我们可以看到一些用户的显示。
下面的截图向我们展示了用户列表页面的样子:
![images/book1/chapter7/1.jpg](https://i-blog.csdnimg.cn/blog_migrate/e748aa9cbb1993677f2429f50fb6d26c.jpeg)
我们也可以通过http://localhost/trackstar/index.php?r=user/create来访问新建用户表单。如果目前你未登录,你将被引导至登录页面,登录后才可显示新建表单。使用demo/demo 或admin/admin 帐号密码登录后来访问这个表单。
从第一次在Project上使用CRUD操作,到后来的Issue,我们对于这些功能如何在Gii代码生成器下执行已经非常熟悉了。那些输入框提供的新建和更新操作是一个不错的开始,不过通常需要进行一些特殊的修改来达到预期的项目需求。在新建用户表单时,这个原则一样适用。每一个tbl_user表中定义的列,都有一个输入框与之对应,我们并不希望把它们全部暴露给用户。last_login_time, creation_time,create_user_id, update_time, update_user_id这些项应该被设置成在表单被提交后由程序自动附值。
审查之前的表更新公共字段
回顾第五,第六章,在我们介绍基于Project和Issue的CRUD功能时,我们也注意到我们的表单提供的输入项远超过了应该暴露给用户的。当我们完成设置时,我们所有的数据库表都拥有相同的新建、更新时间和用户列,每一个自动生成的表单都有相同程度的暴露。在第五章处理创建Project时,我们完全忽略了这些输入框。同样的,在第六章建立一个新Issue时,我们只是从表单里移除了这些输入框,当一个新行被添加时,我们并没有设置任何逻辑代码来为这些框设置合适的值。
让我们花费一点时间来添加这些逻辑代码。因为我们所有的实体表(tbl_project, tbl_issue, 和tbl_user)都定义了相同的列,我们将添加所需的逻辑代码到一个公共基类,然后每一个独立的AR类都继承自这个公共基类。
可能你也想到了,在我们开始添加项目代码之前我们先要写一个测试。我们已经完成了位于tests/unit/ProjectTest.php的ProjectTest::testCreate()的测试函数,用来测试新建Project。我们将通过修改这个测试方法来测试更新公共列的操作。
首先应当修改移除ProjectTest::testCreate()方法里的setAttributes()方法中,在新建Project时对这些列的直接赋值:
$newProject->setAttributes(array( 'name' => $newProjectName, 'description' => 'This is a test for new project creation', // - remove - 'createTime' => '2009-09-09 00:00:00', // - remove - 'createUser' => '1', // - remove - 'updateTime' => '2009-09-09 00:00:00', // - remove - 'updateUser' => '1', ));
现在我们需要添加用户ID和移除当保存AR时的false参数返回,因为我们现在要触发验证。之所以要在这里触发AR验证是为了更新这些输入框,我们需要引入验证流。下面的代码显示了全部方法,同时高亮了其中修改部分。
public function testCreate() { //CREATE a new Project $newProject=new Project; $newProjectName = 'Test Project Creation'; $newProject->setAttributes(array('name' => $newProjectName, //这行 'description' => 'This is a test for new project creation', //这行 )); //这行 //set the application user id to the first user in our users fixture data Yii::app()->user->setId($this->users('user1')->id); //这行 //save the new project, triggering attribute validation $this->assertTrue($newProject->save()); //这行 //READ back the newly created Project to ensure the creation worked $retrievedProject = Project::model()->findByPk($newProject->id); $this->assertTrue($retrievedProject instanceof Project); $this->assertEquals($newProjectName, $retrievedProject->name); //ensure the user associated with creating the new project is the same as the applicaiton user we set //when saving the project $this->assertEquals(Yii::app()->user->id, $retrievedProject->create_user_id); //这行 }
新添加的断言是用来测试Project表的create_user_id列是否使用了当前用户ID做更新时的属性。这已经足够证明我们的做法是可行的。如果你以命令行方式运行该命令,你将看到我们所预期的失败提示。失败的原因是我们还未为该字段设置相关逻辑代码。
现在是让这个测试通过的时候了。我们着手新建一个用来存放更新相关公共字段逻辑代码的类。这个新类将成为一个可被我们应用中所有AR类继承的基类。新建这样一个基类而不是直接将相关逻辑代码添加到Project模型类的原因是:这部分逻辑代码被Issue和User模型类所共同需要。比起复制相同代码到每一个类中,上面的做法将允许我们在一个地方为每一个AR模型类同时设置那些字段(属性)。我们也将定义这个类为抽象类,这样使得他们不可以被直接实例化。
我们需要在protected/models/ 手工新建一个文件TrackStarActiveRecord.php,并按如下形式添加代码:
<?php abstract class TrackStarActiveRecord extends CActiveRecord { /** * Prepares create_time, create_user_id, update_time and * update_user_ id attributes before performing validation. */ protected function beforeValidate() { if ($this->isNewRecord) { // set the create date, last updated date // and the user doing the creating $this->create_time = $this->update_time = new CDbExpression('NOW()'); $this->create_user_id = $this->update_user_id = Yii::app()->user->id; } else { //not a new record, so just set the last updated time //and last updated user id $this->update_time = new CDbExpression('NOW()'); $this->update_user_id = Yii::app()->user->id; } return parent::beforeValidate(); } }
在这里我们重载了CActiveRecord::beforeValidate()方法。这是CActiveRecord提供的众多可以定制其工作流的事件之一。作为一个小提示,如果在AR类save()被调用时不提供一个false参数,验证将被触发。这一过程将执行所有在AR类中rules()里定义的验证细节。一共有2个方法允许我们将适当的逻辑置放在验证开始之前和验证结束之后,这个2方法为:beforeValidate()和afterValidate()。在这个部分,我们决定在验证执行前对我们审查表后的公共字段进行直接设置。
你可能注意到在之前的代码中使用了CDbExpression来设置新建和更新时间的值为Unix时间戳。自1.0.2版开始,一个属性的值可以设置为CDbExpression类型,在所属记录被保存前。在保存过程中所需的值将由运行数据库表达式获得。
上述代码中的NOW是MySQL专有函数,如果你使用非MySQL数据库,它可能不会起作用。你可以使用不同的方法来设置这个值。例如,使用PHP时间函数,并且通过格式化使其符合列时间类型:
$this->createTime=$this- >updateTime=date( 'Y-m-d H:i:s', time() );
无论在我们处理新建(如insert)还是更新(如update)的时候,都设置相关字段的值。同时我们也保证父类中的方法通过parent::beforeValidate()被执行,使得它可以完成所有工作。
在完成上述功能后,我们需要对3个已存在AR类做修改(Project.php, User.php, Issue.php),使之继承自新建的抽象类,而不是直接继承自ACtiveRecord。例如,如下操作:
class Project extends CActiveRecord {
我们将其改成如下格式:
class Project extends TrackStarActiveRecord {
对其他模型类进行相同修改。修改Project类后,返回确认测试通过。
都完成后,我们可以针对新建Projects, Issues,和Users表单移除一些字段的输入框(Issues表单已经在之前迭代中移除)。各自的HTML结构代码可以在protected/views/project/_form.php, protected/views/issue/_form.php, 和 protected/views/user/_form.php中找到。从每一个HTML代码中移除以下代码:
<div class="row"> <?php echo $form->labelEx($model,'create_time'); ?> <?php echo $form->textField($model,'create_time'); ?> <?php echo $form->error($model,'create_time'); ?> </div> <div class="row"> <?php echo $form->labelEx($model,'create_user_id'); ?> <?php echo $form->textField($model,'create_user_id'); ?> <?php echo $form->error($model,'create_user_id'); ?> </div> <div class="row"> <?php echo $form->labelEx($model,'update_time'); ?> <?php echo $form->textField($model,'update_time'); ?> <?php echo $form->error($model,'update_time'); ?> </div> <div class="row"> <?php echo $form->labelEx($model,'update_user_id'); ?> <?php echo $form->textField($model,'update_user_id'); ?> <?php echo $form->error($model,'update_user_id'); ?> </div>
并且在创建的用户表单中(protected/views/user/_form.php),我们也可以同时移除last_login_time部分:
<div class="row"> <?php echo $form->labelEx($model,'last_login_time'); ?> <?php echo $form->textField($model,'last_login_time'); ?> <?php echo $form->error($model,'last_login_time'); ?> </div>
完成上述移除后,我们需要移除rules方法中的对应验证规则。这些验证规则可以确保用户输入的格式是正确的。因为这些字段无需用户输入,所以我们可以移除对应规则。
在User::rules()方法中,移除以下2项规则:
array('create_user_id, update_user_id', 'numerical', 'integerOnly'=>true), array('last_login_time, create_time, update_time', 'safe'),
在Project和Issue中也定义了相似的规则,但非完全一致。确保仅保留需要用户填写的项被施加验证规则。
移除last_login_time属性是有原因的。允许用户输入这个属性不太合适。这个属性应该仅在成功登录时被更新。当我们打开视图文件并移除相关字段的时候,这一项也被决定移除掉。同时,与之相关的逻辑代码添加工作,将放在我们完成一些其他修改和主题之后。
虽然我们一直在修改User类的验证规则,但还有另外一个需要修改的地方。我们希望确保每一个用户的email都和他的username一样唯一,这一验证应该在表单提交时被执行。针对rules()进行如下修改
array('email, username', 'unique'),
完成后的User::rules 如下:
public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( array('email', 'required'), array('email, username, password', 'length', 'max'=>256), array('email, username', 'unique'), // The following rule is used by search(). // Please remove those attributes that should not be searched. array('id, email, username, password, last_login_time, create_time, create_user_id, update_time, update_user_id', 'safe', 'on'=>'search'), ); }
上述规则中的特殊申明是对Yii内部验证器CUniqueValidator的一个引用。这样的验证确保了对应底层数据库表的类属性唯一性。添加这条验证规则后,当我们输入一个已存在的username或email时,会出现错误。当我们在第6章新建tbl_user表时,我们插入了2个用户,所以你可以基于这个做下尝试。其中一个用户email为test1@notanaddress.com,使用这个email地址添加一个新用户。你会看到如下图的错误消息,同时错误位置会被高亮提示。
![images/book1/chapter7/2.jpg](https://i-blog.csdnimg.cn/blog_migrate/1bdfd07b74f3d64b19f8591c18c70bd0.jpeg)
添加确认密码输入框
我们需要添加一个新输入框来强制用户确认他输入的密码。这是一次针对用户注册表单的标准练习,同时也可以帮助用户避免在输入这部分重要信息时发生错误。幸运地是,Yii的另外一个内部验证器(CCompareValidator),可以完成你所期望的工作。他的工作是对比2个属性值,并且在不相等时返回一个错误消息。
为了使用这一内置验证器,我们需要为我们的模型类添加一个新属性。在User模型AR类顶部添加如下代码:
public $password_repeat;
我们在期望对比的属性名后附加_repeat,形成了与之对比的新属性名。对比验证器允许你设置2个属性名,或者属性名和确定值进行对比。在没有明确指出对比规则的情况下,默认对比属性名相同,后接_repeat的属性。这也是我们为何如此命名的用意。现在我们可以按照如下方式在User::rules()添加验证器:
array('password', 'compare'),
我们希望标识所有输入框都为必须填写。当前,只有email属性被标识为必填。所以我们针对User::rules()进行修改,将username和password添加到验证列表中 :
array('email, username, password', 'required'),
由于我们直接在User AR类中添加了$password_repeat属性,并且它与底层数据库表之间没有对应关系,我们需要告诉模型类允许这个属性在setAttributes()被调用时被设置。我们的做法是将其添加到User模型类的安全属性列表中。向User::rules()数组添加下列代码:
array('password_repeat', 'safe'),
简单介绍一下,当表单提交到UserController::actionCreate()方法时,会使用下列的代码将User 模型类的属性进行批量转换:
$model->attributes=$_POST['User'];
在这里,所有$_POST[‘User’]数组里的项会和$model类安全属性列表中的做匹配,成功匹配的完成赋值。默认的,除了主键外的所有底层数据库表里的字段都被视为安全的。我们新建的$password_repeat不存在对应的tbl_user表中的列,需要将其直接添加到安全属性列表。
我们还需要将密码确认框添加到表单,让我们行动起来。
在HTML表单中添加如下新代码,打开 protected/views/user/_form. php,并且在密码输入框下面添加如下代码块:
<div class="row"> <?php echo $form->label($model,'password_repeat'); ?> <?php echo $form->passwordField($model,'password_repeat',array('size'=>60,'maxlength'=>256)); ?> <?php echo $form->error($model,'password_repeat'); ?> </div>
当所有的表单都被修改后,新建用户表单显示如下图:
![images/book1/chapter7/3.jpg](https://i-blog.csdnimg.cn/blog_migrate/fe9f59681ce2ece3dd884ea7d51c07f9.jpeg)
现在如果你在,password和password Repeat输入框里填入不同的值,我们将会看到如下的错误信息:
![images/book1/chapter7/4.jpg](https://i-blog.csdnimg.cn/blog_migrate/0dcb6ab097adfad5cf260a289f649d28.jpeg)
为密码加密
针对整个新建用户流程的最后一个修改项是在保存之前对用户输入的密码字段进行加密。基于安全标准观点出发,在数据被保存之前,最低限度我们可以对密码运行一次单向加密算法。我们将通过重载CActiveRecord类下的允许自定义AC工作流程的方法来把相关逻辑添加到User.php AR类中去。这次我们将重载afterValidate()方法,并且在我们保存数据之前,简单的对密码进行MD5加密。
打开User AR类,在类文件底部添加如下代码:
/** * perform one-way encryption on the password before we store it in the database */ protected function afterValidate() { parent::afterValidate(); $this->password = $this->encrypt($this->password); } public function encrypt($value) { return md5($value); }
当修改完成后,整个流程为当所有其他属性都通过验证之后,密码会被进行简单的单向MD5加密。
在新添一条记录时,上述修改不会有任何问题,但是在更新时,它有可能对已加密值进行重复加密。对此我们有多种处理方法,但是为了保持简单,我们会要求用户每次更新他们的用户信息时提供一个用于认证的密码。
至此,我们有足够的能力为我们的项目添加新用户,通过使用Gii工具里的Crud Generator命令添加此项功能,同时也添加了读取,更新,和删除用户的功能。做一个简单的练习,添加一些用户,读取所有的用户列表,更新其中的一些用户资料,最后删除部分用户,确保用户功能按照预期目标工作。另外,在执行删除工作时,请使用admin登录,而不是demo。
使用数据库进行用户认证
正如我们所知道的,在我们使用yiic新建项目时,建立基础登录表单和用户认证流程。这个认证方式非常简单。只有在输入demo/demo或admin/admin时,认证才可以通过,其他情况下都是失败。这显然不是一个长期解决方案,但是是一个很好的基础。我们通过修改已经存在认证流程使其使用已经插入数据的数据表(这些表已经是我们模型的一部分)来完成认证。但在我们改变默认的实现方式之前,让我们仔细观察一下Yii如何实现一个认证模型。
介绍Yii认证模型
Yii认证框架的核心是一个叫user的应用组件,一般来说,是IWebUser接口的对象实现。这个具体的类是默认通过框架类CWebUser实现的。这个用户组件封装了所有当前用户在整个应用中的认证信息。这个组件的配置在我们使用yiic工具自动生成整个项目代码的时候被创建。具体配置信息可以在protected/config/main.php文件查看,基于components数组:
'user'=>array( // enable cookie-based authentication ' allowAutoLogin'=>true, ),
因为它被配置成了一个使用关键词‘user’的应用组件,所以我们可以在我们项目中的任何地方通过Yii::app()->user进行访问。
我们也发现了类属性allowAutoLogin也在这里被设置好了。这个属性默认值为false,但是当设置为true时,可以将用户的信息持久的储存在浏览器的cookies里。储存的信息将在后面的访问中自动用于认证。这样我们就可以在登录表单显示一个Remember Me复选框,当用户选中的情况下,再次访问网站都会自动登录进行。
Yii认证框架定义了一个独立的实体来储存当前认证逻辑。这是一个认证类,而且一般形式上,这个类实现了IUserIdentity接口。这个类扮演的主要角色是封装了认证逻辑,使得容易进行不同的应用。基于项目需求,我们需要将用户数据的用户名和密码和数据库中储存的值进行匹配,或者允许用户使用他们的 OpenID credentials进行登录,或者整合入一个现有的LDAP方式。逻辑分离是指将认证方式从登录流程中分离出来,从而允许我们轻松的在不同应用中转换。认证类提供这样程度的分离。
当最初建立我们的应用程序时,处于protected/components/UserIdentity.php的认证类被同时创建。它继承自Yii框架类CUserIdentity,这个框架类是基于用户名密码认证的基础类。让我们仔细观察一下这个类被生成时的代码情况:
<?php /** * UserIdentity represents the data needed to identity a user. * It contains the authentication method that checks if the provided * data can identify the user. */ class UserIdentity extends CUserIdentity { /** * Authenticates a user. * The example implementation makes sure if the username and password * are both 'demo'. * In practical applications, this should be changed to authenticate * against some persistent user identity storage (e.g. database). * @return boolean whether authentication succeeds. */ public function authenticate() { $users = array( // username => password 'demo' => 'demo', 'admin' => 'admin', ); if (!isset($users[$this->username])) $this->errorCode = self::ERROR_USERNAME_INVALID; else if ($users[$this->username] !== $this->password) $this->errorCode = self::ERROR_PASSWORD_INVALID; else $this->errorCode = self::ERROR_NONE; return!$this->errorCode; } }
大量的工作是通过定义在一个认证类中的authenticate()方法中实现的。这里就是我们存放身份认证代码的地方。这里的实现方式是使用硬编码的用户名/密码对(demo/demo 和 admin/admin)。它会将这些值与类属性(定义在父类CUserIdentity中的属性)的用户名和密码进行匹配,如果匹配失败,将设置并返回一个适当的错误代码。
为了更好的理解,这一部分是如何适应整个端到端认证流程,让我们从登录表单开始本次认证之旅。如果我们进入登录页:http://localhost/trackstar/index.php?r=site/login,我们将看到一个允许输入帐号密码,还有之前讨论过的Remember Me复选框的登录表单。提交这个表单,触发处于SiteController::actionLogin()方法中的逻辑代码。下面的时序图描述了登录成功时,类之间的交互过程。
![images/book1/chapter7/5.jpg](https://i-blog.csdnimg.cn/blog_migrate/2d0391a754678a8b3a166eb363cf8254.jpeg)
这一流程始于将表单提交值设置为表单类模型LoginForm的属性。LoginForm->validate()方法此时被调用,并使用rules()方法中设置的规则对设置的属性值进行验证。rules()定义如下:
public function rules() { return array( // username and password are required array('username, password', 'required'), // rememberMe needs to be a boolean array('rememberMe', 'boolean'), // password needs to be authenticated array('password', 'authenticate'), ); }
最后一条规则规定password属性必须使用自定义方法authenticate()进行验证,这个方法按照如下代码定义在LoginForm类中:
/** * 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', 'Incorrect username or password.'); }
按时序图的顺序,LoginForm类中的密码验证规则调用同一个类中的authenticate()方法。这个方法创建了一个被使用的认证类实例,在这个案例中是/protected/components/UserIdentity.php,然后调用自身的authenticate()方法。此方法UserIdentity::authenticate()定义如下:
/** * Authenticates the password. * This is the 'authenticate' validator as declared in rules(). */ public function authenticate($attribute, $params) { if (!$this->hasErrors()) { // we only want to authenticate when noinput errors $identity = new UserIdentity($this->username, $this->password); $identity->authenticate(); switch ($identity->errorCode) { case UserIdentity::ERROR_NONE: $duration = $this->rememberMe ? 3600 * 24 * 30 : 0; // 30 days Yii::app()->user->login($identity, $duration); break; case UserIdentity::ERROR_USERNAME_INVALID: $this->addError('username', 'Username is incorrect.'); break; default: // UserIdentity::ERROR_PASSWORD_INVALID $this->addError('password', 'Password is incorrect.'); break; } } }
这里实现了使用用户名和密码进行认证。在这次实现中,在输入用户名密码对为demo/demo 或 admin/admin的情况下,这个方法将返回true。等我们登录成功,认证执行完毕,在应用组件中的login()方法被调用。
正如所说,web应用程序默认被设置为使用Yii框架的CWebuser用户组件。它的login()方法被放在一个认证类里,并有一个可选参数控制浏览器cookie生存时间。在上面的代码中,我们发现它将30天设置为勾选Remember Me后的生存时间。设置为0,则取消cookie功能。
在用户会话期间,login方法将认证类中包含的信息持久的储存。默认的储存在PHP的session中。
当这些都完成之后,LoginForm中的validate()方法将返回true(成功登录的情况下)。控制器将重定向URL到Yii::app()->user->returnUrl的值。你可以定义到一个指定页面,如果你希望用户重定向回他们以前的页面,也就是说,任何他们发起登录的页面。默认值为项目的实体URL。
修改认证的实现方法
现在了解了整个认证过程后,我们现在可以轻松的找出针对表tbl_user进行如何的修改,才能验证登录表单提供的用户名和密码。我们可以简单的修改user认证类的authenticate()方法,来验证存在的用户名密码和提供的是否匹配。从现在开始,除了认证方法我们的UserIdentity.php类中什么都没有,让我们使用如下代码覆盖整个文件的内容:
<?php /** * UserIdentity represents the data needed to identity a user. * It contains the authentication method that checks if the provided * data can identify the user. */ class UserIdentity extends CUserIdentity { private $_id; /** * Authenticates a user using the User data model. * @return boolean whether authentication succeeds. */ public function authenticate() { $user = User::model()->findByAttributes(array('username' => $this->username)); if ($user === null) { $this->errorCode = self::ERROR_USERNAME_INVALID; } else { if ($user->password !== $user->encrypt($this->password)) { $this->errorCode = self::ERROR_PASSWORD_INVALID; } else { $this->_id = $user->id; if (null === $user->last_login_time) { $lastLogin = time(); } else { $lastLogin = strtotime($user->last_login_time); } $this->setState('lastLoginTime', $lastLogin); $this->errorCode = self::ERROR_NONE; } } return!$this->errorCode; } public function getId() { return $this->_id; } }
新代码中的一些地方是需要被指出的。首先,它通过实例化User模型AR类来试图取出tbl_user中的一行,其中的username和UserIdentity类的属性相同(还记得那些来自登录框的值嘛?),因为我们要求了新建用户时用户名的唯一性,所以配置的行最大值为1。如果没有找到匹配行,一个用户名错误的消息将会被显示。如果找到了匹配行,将会对密码进行匹配。因为我们对存入数据库的密码进行了单向加密,所以必须使用我们之前添加至User类的encrypt()方法进行加密。如果密码不匹配,将显示一个密码错误的消息。
如果认证成功,在方法提供返回之前,还有一些事发生了。首先,我们为UserIdentity类添加了新属性用户ID。父类的默认实现是返回ID对应的用户名。因为我们正在使用数据库,并且有多个独立的主键作为用户标识,我们想确保这个用户ID,是在任何地方需要调用用户ID时提供的那个。亦即,当Yii::app()->user->id这行代码被执行,数据库中唯一的用户ID被返回,而不是用户名。
扩展应用中的用户属性
下面要说的事是,用户认证类的一个属性是取自数据库的最后登录时间。user应用组件CWebUser,通过定义在标识类中的ID和username获取它的用户属性,这样形如name=>value设置在了叫identity states的数组中。这些是额外的用户属性值,需要被持久保存在用户的会话里。举个例子,我们将属性lastLoginTime指向数据库中last_login_time字段的值。如此,在应用程序的任何地方,这个属性可以通过如下获取:
Yii::app()->user->lastLoginTime;
因为插入新用户时,最后登录时间的值为空,正因为有一个空属性,当用户第一次登录时可以填入适当的时间作为值。我们也尽量将时间格式为更易读。
采用不同的方法储存对应ID的最后登录时间是因为:id为CUserIdentity类的明确定义属性。所以与姓名和ID不同,所有其他需要被持久化保持在会话中的用户属性可以采用相同的方法定义。
当基于cookie认证被开启(通过设置CWebUser::allowAutoLogin值为true),用户认证状态将被储存在cookie中。因此,你不应该使用我们储存用户最后登录时间的方法储存敏感信息(例如,密码)。
当上述改变都完成后,你应该提供正确的储存在表tbl_user中的用户名和密码对来进行登录。使用之前的demo/demo和admin/admin自然是不会工作的。你可以试一下。你可以使用你在本章前面建立的任何帐号进行登录。如果你一直跟随我们的教程,那么我们的数据库相同,以下的帐号密码应该可以工作:
Username: Test_User_One
Password: test1
现在我们已经修改了登录流程,使之使用数据库验证,我们没有进行删除Project, Issue, 或用户实体的权限。这是因为,授权检查要求管理员用户才可以进行该操作。目前,我们数据库中没有管理员用户。不过别担心,这个问题将在下一次迭代中得到解决,很快我们就可以使用这一功能了。
更新用户最后登录时间
正如我们在这一章节前期提到的,我们从新建用户表单中移除了最后登录时间的输入框,但是相关的逻辑并未添加。因为我们使用表tbl_user来跟踪用户最后登录时间,所以我们需要根据最后一次成功登录信息更新这一字段。因为真正的登录发生在LoginForm::login()中,让我们在这个方法中设置这个值。为LoginForm::login()方法添加如下高亮代码:
/** * 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); User::model()->updateByPk($this->_identity->id, array('last_login_time' => new CDbExpression('NOW()'))); //这行 return true; } else return false; }
这里我们使用了updateByPk()方法作为一种高效,处理用户记录更新的方式,并且简单到只需要指出主键和需要更新的name=>value键值对。
在首页上显示最后登录时间
现在,我们已经将最后登录时间储存进了数据库,同时也将其持久的储存在了会话中,让我们接下来将这一时间显示在用户成功登录后看到的欢迎页面上。因所有的一切都在按预期进行,我们会有一个不错的感觉。
打开用来显示首页默认视图文件:protected/views/site/index.php。添加位于欢迎语句下面的高亮代码到文件中:
<h1>Welcome to <i><?php echo CHtml::encode(Yii::app()->name); ?></i></h1> <?php if (!Yii::app()->user->isGuest): ?> <p> You last logged in on <?php echo date('l, F d, Y, g:i a', Yii::app()->user->lastLoginTime); ?>. </p> <?php endif; ?>
正如我们所见的,让我们删除其他在我们添加的代码之下的自动生成的帮助代码。一旦你完成了修改,并且再次登录,你将看到和下面截图一样的画面(欢迎语句下面有你最后一次成功登录的时间)
![images/book1/chapter7/6.jpg](https://i-blog.csdnimg.cn/blog_migrate/36a64d35d2745b9ab52ec183577a96b2.jpeg)
小结
这次迭代是我们针对用户管理的2次迭代(认证和授权)之一。我们通过一步一步修改新建用户流程,为项目用户建立了管理CRUD操作的能力。我们为全部AR类建立了一个基类,因此我们可以轻松的管理在所有表中审查出来公共列。我们也通过更新代码实现了将最后登录时间存进数据库的管理。通过这次迭代,我们了解了CActiveRecord验证工作流程以及在验证之前或之后进行其它处理。
接下来我们致力于理解Yii的认证模型,使之符合我们的项目预期:使用数据库中储存的值与用户数据的帐号密码进行匹配。
至此,我们完成了认证部分,我们可以进入Yii认证和授权框架的第二部分——授权的学习中了。这部分的学习将在下次迭代中介绍。