《Agile Web Application Development with Yii1.1 and PHP5》

Chapter 7: Iteration 4: User Management And Authentication

We have made a lot of progress in a short amount of time. The basic functionality foundations for our TrackStar application have been laid. We now have the ability to manage projects and issues within projects, and this is the primary purpose of this application. Of course, there is still much left to do.

Back in Chapter 3, when we were introducing this application, we described it as a user-based application that allows for the creation of user accounts, and grants access to the application features once a user has been authenticated and authorized. In order for this application to be useful to more than one person we need to add the ability to manage users within projects. This is going to be the focus of the next two iterations.

Iteration planning

When we used the yiic command line tool to initially create our TrackStar application, we noticed that basic login functionality was automatically created for us. The login page allows for two username/password credential combinations, demo/demo and admin/admin. You may recall that we had to log in to the application in order to perform some of our CRUD operations on our project and issue entities.

This basic authentication skeleton code does provide a good start, but we need to make a few changes in order to support any number of users. We also need to add user CRUD functionality to the application to allow us to manage these multiple users. This iteration is going to focus on extending the authentication model to use the User table and add the needed functionality to allow for basic user data management.

In order to achieve the above outlined goals, we should identify all the more granular items we will work on within this iteration. The following list identifies these items:

    Create the controller classes that will house the functionality to allow us to: ° Create new users ° Fetch a list of existing users from the database ° Update/edit existing users ° Delete existing users • Create the view files and presentation tier logic that will: ° Display the form to allow for new project creation ° Display a listing of all the existing projects ° Display the form to allow for a user to edit an existing project ° Add a delete button to the project listing to allow for project deletion • Make adjustments to the create new user form so that it can be used by external users as a self-registration process • Alter the authentication process to use the database to validate the login credentials

Running the test suite

It's always best to run our test suite before we start adding new functionality. With each iteration, as we add to our application functionality, we add to our test suite. As our test suite grows, so does our application's ability to provide us feedback on its general health. Making sure everything is still working as expected will boost our confidence as we begin making changes. From the tests folder, /protected/tests/, run all unit tests as once:

% phpunit unit/ 
PHPUnit 3.4.12 by Sebastian Bergmann. 
.......... 
Time: 0 seconds OK (10 tests, 26 assertions)

Everything looks good, so let's dive in to this iteration.

Creating our User CRUD

As we are building a user-based web application, we must have the means to add and manage users. We added a tbl_user table to our database model in Chapter 6. You may recall that we left it as an exercise for the reader to create the associated AR model class. If you are following along and did not create the needed user model class, you will need to do so now.

As a brief reminder on using the Gii code creation tool to create the model class. Navigate to the Gii tool via http://localhost/trackstar/ index.php?r=gii and choose the Model Generator link. Leave the table prefix as tbl. Fill in the Table Name field as tbl_user, which will auto-populate the Model Class name field as User. Once the form is filled out, click the Preview button to get a link to a popup that will show you all of the code about to be generated. Then click the Generate button to actually create the new User.php model class file in the /protected/models/ directory

With the User AR class in place, creating the CRUD scaffolding is a snap. As we have done previously, we will once again we lean on the Gii code generation tool for this. As a reminder, here are the necessary steps:

    Navigate to the tool via http://localhost/trackstar/index.php?r=gii. 2. Choose the Crud Generator link from the list of available generators. 3. Type in User for the Model Class name field. The corresponding Controller ID will auto-populate with user. 4. You will then be presented with options to preview each file prior to generating. When satisfied, click the Generate button, which will generate all of the associated CRUD files in their proper locations.

With this in place, we can view our user listing page at http://localhost/ trackstar/index.php?r=user. In the previous iteration, we manually created a couple of users in our system, so that we could properly handle the relationships between projects, issues and users. So, we should see a couple of users listed on this page.

The following screenshot shows how this page is displaying for us:

We can also view the new Create User form by visiting http://localhost/ tasctrak/index.php?r=user/create. If you are not currently logged in, you will be routed to the login page before being able to view the form. So you might have to log in using demo/demo or admin/admin to view this form.

Having created and used our CRUD operation functionality first on our project entity, and then again with Issues, we are very familiar at this point with how these features are initially implemented by the Gii code generation tool. The input forms provided for creating and updating are a great start, but often need some adjusting to meet the specific application requirements. The form generated for creating a new user is no exception. It has an input form field for every single column that has been defined in the tbl_user table. We don't want to expose all of these fields for user input. The columns for last login time, creation time and user, and update time and user should all be set programmatically after the form is submitted.

Updating our common audit history columns

Back in Chapters 5 and 6, when we introduced our Project and Issue CRUD functionality, we also noticed that our forms had more input fields than they should. As we have defined all of our database tables to have the same creation and update time and user columns, every one of our auto-created input forms has these fields exposed. We completely ignored these fields when dealing with the project creation form back in Chapter 5. Then, with the new issue creation form in Chapter 6, we removed the fields from in the form, but we never added the logic to properly set these values when a new row is added.

Let's take a minute to add this logic. As all of our entity tables—tbl_project, tbl_issue, and tbl_user—have the same columns defined, we will add the required logic to a common base class and then have each of the individual AR classes extend from this new base class.

As you might have guessed, we'll write a test first before we start adding in the needed application code. We already have a test in place ProjectTest::testCreate(), in tests/unit/ProjectTest.php, for testing the creation of a new project. We'll alter this existing test method to test our new process of updating our common audit history columns.

The first thing we need to change in our ProjectTest::testCreate() method is to remove the explicit setting of these columns when we call the setAttributes() method for the newly created 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',
));

Now we need to add in the explicit setting of the user ID and remove the false parameter sent when saving the Active Record, as we now want the validation to be triggered. The reason we want to trigger the AR validation is because we are going to tap into the validation workflow in order to update these fields. The code below shows the entire method with the new changes highlighted:

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);
}

The new assertion added is testing that the create_user_id column of the Project was properly updated with the current application's user ID. This should be enough to confirm that our approach is working. If you now run this test from the command line, you should see the test fail, which is what we expect. The test fails because we have yet to add in the logic required to set this field.

Now let's get this test to pass. We are going to create a new class to house the logic needed to update our common audit history fields. This class is going to be a base class from which all our application AR classes can extend. The reason we are creating this new class, rather than just adding the logic directly to our Project model class, is because our other model classes, Issue and User, also need this logic. Rather than duplicate the code in every AR model class, this approach will allow us to properly set these fields for every AR model class in just one place. We will also make this new class abstract, as it should not be instantiated directly.

We need to manually create a new file, protected/models/ TrackStarActiveRecord.php, and add the following code::

<?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();
    }
 
}

Here we are overriding the CActiveRecord::beforeValidate() method. This is one of the many events that CActiveRecord exposes to allow customization of its process workflow. As a quick reminder, if you do not explicitly send false as a parameter when calling the save() method on an AR class, the validation process will be triggered. This process performs the validations as specified in the rules() method within the AR class. There are two methods exposed that allow us to tap in to the validation workflow and perform any necessary logic either right before or right after the validation is performed: beforeValidate() and afterValidate(). In this case, we have decided to explicitly set our audit history fields just prior to performing the validation.

You probably noticed the use of CDbExpression in the previous code to set the timestamp for both the creation and update time. Starting from version 1.0.2 of Yii, an attribute can be assigned a value of CDbExpression type before the record is saved. That database expression will then be executed to provide the value for the attribute during the saving process.

Using NOW() in the previous code is specific to MySQL. This may not work if you are following along using a different database. You can always take a different approach for setting this value. For example, using the PHP time function and formatting it appropriately for the column's data type: $this->createTime=$this- >updateTime=date( 'Y-m-d H:i:s', time() );

We determine whether or not we are dealing with a new record (that is, an insert) or an existing record (that is, an update) and set our fields appropriately. We then make sure to invoke the parent implementation by returning parent::beforeValidate() to ensure it has a chance to do everything it needs to do.

To try this out, we now need to alter each of the three existing AR classes—Project. php, User.php, and Issue.php—to extend from this new abstract class rather than directly from CActiveRecord. So, for example, rather than the following:

class Project extends CActiveRecord 
{

We need to change it to:

class Project extends TrackStarActiveRecord 
{

And similarly for our other model classes. Once you have done this for the Project model AR class, rerun the tests to ensure they pass.

With this now in place, we can remove these fields from each of the forms for creating new projects, issues, and users (we already removed them from the issues form in the previous iteration). The HTML for these form fields are defined in protected/views/project/_form.php, protected/views/issue/_form.php, and protected/views/user/_form.php respectively. The lines we need to remove from each of these files are the following:

<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>

And from the user creation form, protected/views/user/_form.php, we can also remove the last login time field:

<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>

As we are removing these from being form inputs, we should also remove the validation rules defined for these fields in the associated rules method. These validation rules are defined to ensure the data submitted by the user is correctly formatted. As these fields are not going to be filled in by the user, we can remove the rules.

In the User::rules() method, the two rules we should remove are:

array('create_user_id, update_user_id', 'numerical', 'integerOnly'=>true), 
array('last_login_time, create_time, update_time', 'safe'),

The Project and Issue AR classes have similar rules defined, but not identical. When removing those rules, be sure to leave in the rules that do still apply to the user input fields.

The removal of the rule for the last_login_time attribute above was intentional. We should prevent this from being shown as a user input field as well. This field needs to be updated automatically upon a successful login. As we had the view file open and were removing the other fields, we decided to remove this one now as well. However, we will wait to add the necessary application logic until after we make a few other changes and cover a few other topics.

Actually, while we still have our hands in this validation rules method for the User class, we should make another change. We want to ensure that the e-mail, as well as the username, for every user is unique. We should validate this requirement when the form is submitted. We can add these two rules by adding the following line of code to this rules() method:

array('email, username', 'unique'),

The entire User::rules() method should now look like the following:

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'),
        );
}

The unique declaration in the previous rule is an alias that refers to the Yii's built- in validator, CUniqueValidator. This validates the uniqueness of the model class attribute against the underlying database table. With the addition of this validation rule, we will receive an error when attempting to enter either an e-mail and/or username that has already been entered. When we first created our tbl_user table in Chapter 6, we added two test users, so we would have some data to play with. The first of these two users has an e-mail address of test1@notanaddress.com. Try to add another user using the same e-mail address. The following screenshot shows the error message received and the highlighting of the field in error after such an attempt:

Adding a password confirmation field

We should add a new field to force the user to confirm the password they entered. This is a standard practice on new user registration forms and helps the user avoid making a mistake when entering this important piece of information. Fortunately, Yii comes with another built-in validator, CCompareValidator, which does exactly what you think it might do. It compares the values of two attributes, and returns an error if they are not equal.

In order to take advantage of this built-in validation, we need to add a new attribute to our model class. Add the following attribute to the top of the User model AR class:

public $password_repeat;

We named this attribute by appending _repeat to the name of the attribute we want to compare against. The compare validator will allow you to specify any two attributes to compare, or compare an attribute to a constant value. If no comparison attribute or value is specified when declaring the compare rule, it will default to looking for an attribute beginning with the same name as the one being compared with the addition of _repeat appended to the end. This is why we named the attribute in this manner. Now we can add a simple validation rule to the User::rules() method as follows:

array('password', 'compare'),

We want to mark all of the fields on the form as being required. Currently, our required rule is being applied to the e-mail field only. While we are making changes to this User::rules() method, let's add username and password to this list as well:

array('email, username, password', 'required'),

As we have explicitly added the $password_repeat attribute to the User AR class, and it is not a column in the underlying database table, we need to also tell the model class to allow this field to be set in a bulk manner when the setAttributes() method is called. We do this by explicitly adding our new attribute to the safe attributes list for our User model class. To do this, add the following to the User::rules() array:

array('password_repeat', 'safe'),

To explain this in a little more detail. When our form is submitted back to the UserController::actionCreate() method, it uses the following code to set the User model class attributes in a bulk manner:

$model->attributes=$_POST['User'];

What happens here is that for every key in the $_POST['User'] array that matches the name of a safe attribute in the $model class, that class attribute's value is set to the corresponding value in the array. By default, for a CActiveRecord model class, all underlying database columns, except the Primary Key, are considered safe. As our new $password_repeat is not a column of tbl_user, we need to explicitly add it to this list of safe attributes.

We still need to add this password confirmation field to the form, so let's do that now.

To add this new field to the HTML form, open up protected/views/user/_form. php, and add the following code block below the password field:

<div class="row"> 
    <?php echo $form->label($model,'password_repeat'); ?> 
    <?php echo $form->passwordField($model,'password_repeat',array('si
ze'=>60,'maxlength'=>256)); ?> 
    <?php echo $form->error($model,'password_repeat'); ?>
</div>

With all of these form changes in place, the create new user form should look as depicted in the following screen:

Now, if we attempt to submit the form with different values in the Password and Password Repeat fields, we will be met with an error as shown in the following screenshot:

Adding password encryption

One last change we should make before we leave the new user creation process is to encrypt the password before we store it. It is the least we can do, from a security standpoint, to perform a one-way encryption algorithm on sensitive user information before we add it to persistent storage. We will add this logic to the User.php AR class by taking advantage of another one of CActiveRecord's methods that allow us to customize the default active record workflow. This time we'll override the afterValidate() method and apply a simple MD5 encryption to the password before we save the record.

Open the User AR class and add the following to the bottom of the class:

/**
  * 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);
}

With this in place, it will encrypt the password using a simple one-way MD5 encryption just after all of the other attribute validations are performed.

This approach works fine for brand new records, but for updates, it runs the risk of encrypting an already encrypted value. We could handle this a number of ways, but to keep things simple for now, we will need to ensure we ask the user to supply a valid password every time they desire to update their user data.

We now have the ability to add new users to our application. As we initially created this form using the Gii tool's Crud Generator command, we also have read, update, and delete functionality for users. Try it out by adding some new users, viewing a list of those users, updating their information, and then deleting a few of the entries to ensure everything is working as expected. Remember that you will need to be logged in as admin, as opposed to demo, in order to perform the deletes.

Authenticating users using the database

As we know, a basic login form and user authentication process was created for us simply by using the yiic command to create our new application. This authentication scheme is very simple. It interrogates the input form username/password values, and if they are either demo/demo or admin/admin, the authentication passes, otherwise it fails. This is obviously not intended to be the long term solution, but rather a foundation on which to build. We are going to build upon this by altering the authentication process to use our user database table that we already have as part of our model. But before we start changing the default implementation, let's take a closer look at how Yii implements an authentication model.

Introducing the Yii authentication model

Central to the Yii authentication framework is an application component, called user, which, in the most general case, is an object implementing the IWebUser interface. The specific class used by our default implementation is the framework class, CWebUser. This user component encapsulates all the identity information for the current user of the application. This component was configured for us as part of the auto-generated application code when we initially created our application using the yiic tool. The configuration can be seen in the protected/config/main.php file, under the components array element:

'user'=>array(
    // enable cookie-based authentication '
    allowAutoLogin'=>true,
),

As it is configured as an application component, with the key 'user', we can access it at any place throughout our application using Yii::app()->user.

We also notice that the class property, allowAutoLogin, is being set here as well. This property is false by default, but setting it to true enables user information to be stored in persistent browser cookies. This data is then used to automatically authenticate the user upon subsequent visits. This is what will allow us to have a Remember Me checkbox on the login form so that, if the user chooses, they can be automatically logged in to the application upon subsequent visits to the site.

The Yii authentication framework defines a separate entity to house the actual authentication logic. This is called an identity class, and in its most general form is a class that implements the IUserIdentity interface. One of the primary roles of this class is to encapsulate the authentication logic to easily allow for different implementations. Depending on the application requirements, we may need to validate a username and password against values stored in a database, or allow users to log in with their OpenID credentials, or integrate with an existing LDAP approach. Separating the logic that is specific to the authentication approach from the rest of the application login process allows us to easily switch between such implementations. The identity class provides this separation.

When we initially created our application, the identity class file protected/ components/UserIdentity.php was generated for us. It extends the Yii Framework class, CUserIdentity, which is a base class for authentication implementations that use a username and password. Let's take a closer look at the code that was generated for this class:

<?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;
    }
}

The bulk of the work in defining an identity class is the implementation of the authenticate() method. This is where we place the code that is specific to the authentication approach. This implementation simply uses the hard-coded username/password values of demo/demo and admin/admin. It checks these values against the username and password class properties (properties defined in the parent class, CUserIdentity) and if they don't match, it will set and return an appropriate error code.

In order to better understand how these pieces fit into the entire end-to-end authentication process, let's walk through the logic starting with the login form. If we navigate to the login page: http://localhost/trackstar/index. php?r=site/login, we see a simple form allowing the input of a username, a password and an optional checkbox for the Remember Me Next Time functionality that we discussed before. Submitting this form invokes the logic contained in the SiteController::actionLogin() method. The following sequence diagram depicts the class interaction that occurs during a successful login from the moment the form is submitted.

The process starts with setting the class attributes on the form model class, LoginForm, to the form values submitted. The LoginForm->validate() method is then called, which validates these attribute values based on the rules defined in the rules() method. This method is defined as follows:

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'),
    );
}

The last of these rules stipulates that the password attribute be validated using the custom method authenticate(), which is also defined in the LoginForm class as follows:

/**
 * 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.');
}

Continuing to follow the sequence diagram, the password validation within LoginForm calls the authenticate() method within the same class. This method creates a new instance of the authentication identity class being used, in this case it is /protected/components/UserIdentity.php, and then calls its authenticate() method. This method, UserIdentity::authenticate() is as follows:

/**
 * 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;
        }
    }
}

This is implemented to use the username and password to perform its authentication. In this implementation, as long as the username/password combination is either demo/demo or admin/admin, this method will return true. As we are walking through a successful login, the authentication succeeds and the login() method on the user application component is called.

As mentioned, by default the web application is configured to use the Yii Framework class, CWebuser as the user application component. Its login() method takes in an identity class and an optional duration parameter used to set the time to live on the browser cookie. In the above code, we see that this is set to 30 days if the Remember Me checkbox was checked when the form was submitted. If you do not pass it a duration, it is set to 0. A value of zero will result in no cookie being created.

The login method takes the information contained in the identity class and saves it in persistent storage for the duration of the user session. By default, this storage is the PHP session storage.

After all of this completes, the validate() method on LoginForm that was initially called by our controller class returns true, which indicates a successful login. The controller class then redirects to the URL value in Yii::app()->user->returnUrl. You can set this on certain pages throughout the application if you want to ensure the user be redirected back to their previous page, that is, wherever they were in the application before they decided (or were forced) to log in. This value defaults to the application entry URL.

Changing the authenticate implementation

Now that we understand the entire authentication process, we can easily see where we need to make the change to use our tbl_user table to validate the username and password credentials supplied in the login form. We can simply alter the authenticate() method in the user identity class to verify the existence of a matching row with the supplied username and password values. Since, at the moment, there is nothing else in our UserIdentity.php class except the authenticate method, let's completely replace the contents of this file with the following code:

<?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;
    }
 
}

There are a few things going on with this new code that should be pointed out. First, it is now attempting to retrieve a row from the tbl_user table, by way of creating a new User model AR class instance, where the username is the same as the UserIdentity class attribute value (remember that this is set to be the value from the login form). As we enforced the uniqueness of the username when creating a new user, this should find at most one matching row. If it does not find a matching row, an error message is sent to indicate that the username is incorrect. If a matching row is found, it compares the passwords. As we are encrypting our passwords, it has to use the encryption method, User::encrypt(), that we added to the User class previously. If these do not match, it sets an error message to indicate an incorrect password.

If the authentication is successful, a couple other things happen before the method returns. First, we have set a new attribute on the UserIdentity class for the user ID. The default implementation in the parent class is to return the username for the ID. As we are using a database, and have numeric Primary Keys as our unique user identifier, we want to make sure this numeric ID is what is set and returned throughout the application when the user ID is requested. That is, when the code: Yii::app()->user->id is executed, we want to make sure that the unique ID from the database is returned, not the username.

Extending application user attributes

The second thing happening here is the setting of an attribute on the user identity to be the last login time returned from the database. The user application component, CWebUser, derives its user attributes from the explicit ID and name attributes defined in the identity class, and then from name=>value pairs set in array called the identity states. These are the extra user values that should be persisted throughout a user's session. As an example of this, we are setting the attribute named lastLoginTime to be the value of the last_login_time field in the database. This way, at any place in the application, this attribute can be accessed via:

Yii::app()->user->lastLoginTime;

As the initial user rows go into the table with null values for the last login time, there is a quick check for null so that we can store an appropriate time when the user logs in for the very first time. We have also taken the time to format the date for better readability.

The reason we take a different approach when storing the last login time versus the ID is that id just happens to be an explicitly defined property on the CUserIdentity class. So, other than name and id, all other user attributes that need to be persisted throughout the session can be set in a similar manner.

When cookie-based authentication is enabled (by setting CWebUser::allowAutoLogin to be true), these user identity states will be stored in cookie. Therefore, you should not store sensitive information (for example, password) in the same manner as we have stored the user's last login time.

With these changes in place, you will now need to provide a correct username and password combination for a user defined in the tbl_user table in the database. Using demo/demo or admin/admin will, of course, no longer work. Give it a try. You should be able to log in as any one of the users you created earlier in this chapter. If you followed along and have the same user data as we do, the following credentials should work:

Username: Test_User_One Password: test1

Password: test1

Now that we have altered the login process to authenticate against the database, we won't be able to access the delete functionality for any of our project, issue or user entities. The reason for this is that there are authorization checks in place to ensure that the user is an admin prior to allowing access. Currently, none of our database users have been configured to be admins. Don't worry, authorization is the focus of the next iteration, so we will be able to access that functionality again soon.

Updating the user last login time

As we mentioned earlier in this chapter, we removed the last login time as an input field on the user creation form, but we still need to add the logic to properly update this field. As we are tracking the last login time in the tbl_user database table, we need to update this field accordingly after a successful login. As the actual login happens in the LoginForm::login() method in the form model class, let's update this value there. Add the following highlighted line to the LoginForm::login() method:

/**
 * 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()')));    //this row
        return true;
    }
    else
        return false;
}

Here we are calling its updateByPk() method as an efficient approach to simply update the User record, specifying the Primary Key as well as an array of name=>value pairs for the columns we want to update.

Displaying the last login time on the home page

Now that we are updating the last login time in the db, and saving it to persistent session storage when logging in, let's go ahead and display this time on our welcome screen that a user sees after a successful login. This will also help make us feel better because we know that all of this is working as expected.

Open up the default view file that is responsible for displaying our homepage: protected/views/site/index.php. Add the following highlighted lines of code just below the welcome statement:

<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; ?>

And as we are in there, let's go ahead and remove all of the other autogenerated help text, which is everything below these lines we just added. Once you save your changes and log in again, you should see something similar to the screenshot below, which displays the welcome message following by a formatted time indicating your last successful login:

Summary

This iteration was the first of two iterations focused on user management, authentication and authorization. We created the ability to manage CRUD operations for application users, making many adjustments to the new user creation process along the way. We added a new base class for all of our Active Record classes, so that we can easily manage our audit history table columns that are present on all of our tables. We also updated our code to properly manage the user's last login time, which we are storing in the database. In doing so, we learned about tapping into the CActiveRecord validation workflow to allow for pre and post-validation processing.

We then focused on understanding the Yii authentication model in order to enhance it to meet our application's requirements: that the user credentials be validated against the values stored in the database.

Now that we have covered authentication, we can turn focus to second part of Yii's auth-and-auth framework, authorization. This will be the focus of the next iteration.

评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com