验证和授权(Authentication and Authorization)

如果网页的访问需要用户权限限制,那么我们需要使用验证(Authentication)和授权(Authorization)。验证是指核查某人表明的身份信息是否与系统相合。一般来说使用用户名和密码,当然也可能使用别的表明身份方式,录入智能卡,指纹等等。授权是找出已通过验证的用户是否允许操作特定的资源。一般的做法是找出此用户是否属于某个允许操作此资源的角色。

利用Yii内置的验证和授权(auth)框架,我们可以轻松实现上述功能。

Yii auth framework 的核心一块是一个事先声明的user application component(用户应用部件),实现IWebUser接口的对象。此用户部件代表当前用户存储的身份信息。我们能够通过Yii::app()->user在任何地方来获取它。

使用此用户部件,可以通过CWebUser::isGuest检查一个用户是否登陆。可以login (登陆)或者logout (注销)一个用户;可以通过CWebUser::checkAccess检查此用户是否能够执行特定的操作;还可以获得此用户的unique identifier(唯一标识)和别的身份信息。

1. 定义身份类 (Defining Identity Class)

为了验证一个用户,我们定义一个有验证逻辑的身份类。这个身份类实现IUserIdentity 接口。不同的类可能实现不同的验证方式(例如:OpenID,LDAP)。最好是继承 CUserIdentity,此类是居于用户名和密码的验证方式。

定义身份类的主要工作是实现IUserIdentity::authenticate方法。在用户会话中根据需要,身份类可能需要定义别的身份信息

下面的例子,我们使用Active Record来验证提供的用户名、密码和数据库的用户表是否吻合。我们通过重写getId函数来返回验证过程中获得的_id变量(缺省的实现则是返回用户名)。在验证过程中,我们还借助CBaseUserIdentity::setState函数把获得的title信息存成一个状态。

class UserIdentity extends CUserIdentity
{
    private $_id;
    public function authenticate()
    {
        $record=User::model()->findByAttributes(array('username'=>$this->username));
        if($record===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if($record->password!==md5($this->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$record->id;
            $this->setState('title', $record->title);
            $this->errorCode=self::ERROR_NONE;
        }
        return !$this->errorCode;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}

作为状态存储的信息(通过调用CBaseUserIdentity::setState)将被传递给CWebUser。而后者则把这些信息存放在一个永久存储媒介上(如session)。我们可以把这些信息当作CWebUser的属性来使用。例如,为了获得当前用户的title信息,我们可以使用Yii::app()->user->title(这项功能是在1.0.3版本引入的。在之前的版本里,我们需要使用Yii::app()->user->getState('title'))。

提示: 缺省情况下,CWebUser用session来存储用户身份信息。如果允许基于cookie方式登录(通过设置 CWebUser::allowAutoLogin为 true),用户身份信息将被存放在cookie中。确记敏感信息不要存放(例如 password) 。

2. 登录和注销(Login and Logout)

使用身份类和用户部件,我们方便的实现登录和注销。

// 使用提供的用户名和密码登录用户
$identity=new UserIdentity($username,$password);
if($identity->authenticate())
    Yii::app()->user->login($identity);
else
    echo $identity->errorMessage;
......
// 注销当前用户
Yii::app()->user->logout();

缺省情况下,用户将根据session configuration完成一序列inactivity动作后注销。设置用户部件的allowAutoLogin属性为true和在CWebUser::login方法中设置一个持续时间参数来改变这个行为。即使用户关闭浏览器,此用户将保留用户登陆状态时间为被设置的持续时间之久。前提是用户的浏览器接受cookies。

// 保留用户登陆状态时间7天
// 确保用户部件的allowAutoLogin被设置为true。
Yii::app()->user->login($identity,3600*24*7);

3. 访问控制过滤器(Access Control Filter)

访问控制过滤器是检查当前用户是否能执行访问的controller action的初步授权模式。这种授权模式基于用户名,客户IP地址和访问类型。 It is provided as a filter named as "accessControl".

小贴士: 访问控制过滤器适用于简单的验证。需要复杂的访问控制,需要使用将要讲解到的基于角色访问控制(role-based access (RBAC)).

在控制器(controller)里重载CController::filters方法设置访问过滤器来控制访问动作(看 Filter 了解更多过滤器设置信息)。

class PostController extends CController
{
    ......
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
}

在上面,设置的access control过滤器将应用于PostController里每个动作。过滤器具体的授权规则通过重载控制器的CController::accessRules方法来指定。

class PostController extends CController
{
    ......
    public function accessRules()
    {
        return array(
            array('deny',
                'actions'=>array('create', 'edit'),
                'users'=>array('?'),
            ),
            array('allow',
                'actions'=>array('delete'),
                'roles'=>array('admin'),
            ),
            array('deny',
                'actions'=>array('delete'),
                'users'=>array('*'),
            ),
        );
    }
}

上面设定了三个规则,每个用个数组表示。数组的第一个元素不是'allow'就是'deny',其他的是名-值成对形式设置规则参数的。上面的规则这样理解:createedit动作不能被匿名执行;delete动作可以被admin角色的用户执行;delete动作不能被任何人执行。

访问规则是一个一个按照设定的顺序一个一个来执行判断的。和当前判断模式(例如:用户名、角色、客户端IP、地址)相匹配的第一条规则决定授权的结果。如果这个规则是allow,则动作可执行;如果是deny,不能执行;如果没有规则匹配,动作可以执行。

info|提示:为了确保某类动作在没允许情况下不被执行,设置一个匹配所有人的deny规则在最后,类似如下:

return array(
    // ... 别的规则...
    // 以下匹配所有人规则拒绝'delete'动作
    array('deny',
        'action'=>'delete',
    ),
);

因为如果没有设置规则匹配动作,动作缺省会被执行。

访问规则通过如下的上下文参数设置:

  • actions: 设置哪个动作匹配此规则。

  • users: 设置哪个用户匹配此规则。 此当前用户的name 被用来匹配. 三种设定字符在这里可以用:

    • *: 任何用户,包括匿名和验证通过的用户。
    • ?: 匿名用户。
    • @: 验证通过的用户。
  • roles: 设定哪个角色匹配此规则。 这里用到了将在后面描述的role-based access control技术。In particular, the rule is applied if CWebUser::checkAccess returns true for one of the roles.提示,用户角色应该被设置成allow规则,因为角色代表能做某些事情。

  • ips: 设定哪个客户端IP匹配此规则。

  • verbs: 设定哪种请求类型(例如:GET, POST)匹配此规则。

  • expression: 设定一个PHP表达式。它的值用来表明这条规则是否适用。在表达式,你可以使用一个叫$user的变量,它代表的是Yii::app()->user。这个选项是在1.0.3版本里引入的。

授权处理结果(Handling Authorization Result)

当授权失败,即,用户不允许执行此动作,以下的两种可能将会产生:

  • 如果用户没有登录和在用户部件中配置了loginUrl,浏览器将重定位网页到此配置URL。

  • 否则一个错误代码401的HTTP例外将显示。

当配置loginUrl 属性,可以用相对和绝对URL。还可以使用数组通过CWebApplication::createUrl来生成URL。第一个元素将设置route 为登录控制器动作,其他为名-值成对形式的GET参数。如下,

array(
    ......
    'components'=>array(
        'user'=>array(
            // 这实际上是默认值
            'loginUrl'=>array('site/login'),
        ),
    ),
)

如果浏览器重定位到登录页面,而且登录成功,我们将重定位浏览器到引起验证失败的页面。我们怎么知道这个值呢?我们可以通过用户部件的returnUrl 属性获得。我们因此可以用如下执行重定向:

Yii::app()->request->redirect(Yii::app()->user->returnUrl);

4. 基于角色的访问控制(Role-Based Access Control)

基于角色的访问控制( RBAC的)提供了一种简单而强大 集中访问控制。请参阅[维基文章] (http://en.wikipedia.org/wiki/Role-based_access_control)了解更多详细的RBAC与其他较传统的访问控制模式的比较。

Yii实现通过其authManager 应用程序组件分级RBAC的模式。在下面,我们首先介绍用于这模式的主要概念;我们然后描述了如何设定授权数据;最后,我们看看如何利用授权数据,以进行访问检查。

概览(Overview)

在Yii的RBAC的一个基本概念是authorization item(授权项目)。一个授权项目是一个做某事的许可(如创造新的博客发布,管理用户)。根据其粒度和targeted audience, 授权项目可分为operations(行动),tasks(任务)和 roles(角色)。角色包括任务,任务包括行动,行动是许可是个原子。 例如,我们就可以有一个administrator角色,包括post managementuser management任务。user management 任务可能包括create userupdate userdelete user行动。为了更灵活,Yii也可以允许角色包括其他角色和动作,任务包括其他任务,行动包括其他行动。

授权项目通过名称唯一确定。

授权项目可能与business rule(业务规则)关联。业务规则是一块PHP代码,将在检查访问此项的相关时被执行。只有当执行返回true,用户将被视为有权限此项所代表的许可。举例来说,当定义一项行动updatePost,我们想添加业务规则来检查,用户ID是否和帖子作者ID一样,以便只有作者自己能够有权限更新发布。

使用授权的项目,我们可以建立一个authorization hierarchy(授权等级)。在授权等级中如果项目A包括项目BAB的父亲(或说A继承B所代表的权限)。一个项目可以有多个子项目,也可以有多个父项目。因此,授权等级是一个partial-order图,而不是树型。在此等级中,角色项在最高,行动项在最底,任务项在中间。

一旦有了授权等级,我们可以在此等级中分配角色给应用用户。一个用户,一旦被分配了角色,将 有角色所代表的权限。例如,如果我们指定 administrator角色给用户,他将拥有管理员权限 其中包括post managementuser management (和 相应的操作,如create user) 。

现在精彩的部分开始。在控制器的行动,我们要检查,当前用户是否可以删除指定的发布。利用RBAC等级和分配,可以很容易做到这一点。如下:

if(Yii::app()->user->checkAccess('deletePost'))
{
    // 删除此发布
}

配置授权管理器(Configuring Authorization Manager)

在我们准备定义授权等级和执行访问检查前,我们需要配置 authManager 应用程序组件。Yii 提供两种类型的授权管理器: [ CPhpAuthManager ]和 [ CDbAuthManager ] 。前者使用的PHP脚本文件来存储授权 数据,而后者的数据存储在数据库授权。当我们 配置authManager应用 部件,我们需要指定哪些部件类和部件的初始值。例如,

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'sqlite:path/to/file.db',
        ),
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'connectionID'=>'db',
        ),
    ),
);

然后,我们便可使用Yii::app()->authManager访问authManager应用部件。

定义授权等级(Defining Authorization Hierarchy)

定义授权等级涉及三个步骤:定义授权项目,建立授权项目关系项目,并分配角色给应用用户。authManager 应用部件提供了一整套的API来完成这些任务。

根据不同种类的项目调用下列方法之一定义授权项目:

一旦我们拥有一套授权项目,我们可以调用以下方法建立授权项目关系:

最后,我们调用下列方法来分配角色项目给各个用户:

下面我们将展示一个例子是关于用所提供的API建立一个授权等级:

$auth=Yii::app()->authManager;
 
$auth->createOperation('createPost','create a post');
$auth->createOperation('readPost','read a post');
$auth->createOperation('updatePost','update a post');
$auth->createOperation('deletePost','delete a post');
 
$bizRule='return Yii::app()->user->id==$params["post"]->authID;';
$task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule);
$task->addChild('updatePost');
 
$role=$auth->createRole('reader');
$role->addChild('readPost');
 
$role=$auth->createRole('author');
$role->addChild('reader');
$role->addChild('createPost');
$role->addChild('updateOwnPost');
 
$role=$auth->createRole('editor');
$role->addChild('reader');
$role->addChild('updatePost');
 
$role=$auth->createRole('admin');
$role->addChild('editor');
$role->addChild('author');
$role->addChild('deletePost');
 
$auth->assign('reader','readerA');
$auth->assign('author','authorB');
$auth->assign('editor','editorC');
$auth->assign('admin','adminD');

请注意,我们给updateOwnPost任务关联一个业务规则。在这个业务规则,我们只是检查目前的用户ID是否和指定的帖子的作者ID一样。当执行访问检查时,发布信息在开发者提供$params数组中。

info|信息:虽然上面的例子看起来冗长和枯燥,这主要是为示范的目的。开发者通常需要制定一些用户接口,以便最终用户可以更直观使用它来建立一个授权等级。

访问检查(Access Checking)

为了执行访问检查,我们得先知道 授权项目的名字。例如,如果要检查当前用户是否可以创建一个发布,我们将检查是否有createPost行动的权限。然后,我们调用[ CWebUser : : checkAccess ]执行访问检查:

if(Yii::app()->user->checkAccess('createPost'))
{
    // 创建发布
}

如果授权规则关联了需要额外参数的商业规则,我们同样可以通过他们。例如,要检查如果用户是否可以更新发布,我们将编写

$params=array('post'=>$post);
if(Yii::app()->user->checkAccess('updateOwnPost',$params))
{
    // 更新post
}

使用缺省角色(Default Roles)

注意: 缺省角色的功能是在1.0.3版本引入的。

很多Web应用需要一些很特殊的角色,它们通常需要被分配给几乎每一个用户。例如,我们可能需要为所有注册用户分配一些特殊的权力。假如要象上述方法那样去为每一个用户分配这种角色,我们在维护上将面临很多麻烦。因此,我们采用缺省角色功能来解决这个问题。

所谓缺省角色指的是被隐式分配给每一个用户(包括注册和非注册的用户)的角色。它们无需象前面所描述的那样去被分配给用户。当我们调用CWebUser::checkAccess,缺省角色将首先被检查,就像它们已经被分配给当前用户一样。

缺省角色必须通过CAuthManager::defaultRoles属性进行声明。例如,下面的应用配置声明了两个缺省角色:authenticatedguest

return array(
    'components'=>array(
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'defaultRoles'=>array('authenticated', 'guest'),
        ),
    ),
);

因为缺省角色实质上是被分配给每一个用户的,它通常需要伴随一个业务规则用来确定它是否真正适用某个用户。例如,下面的代码定义了两个角色,authenticatedguest,它们在实质上分别被分配给已通过验证和未通过验证的用户。

$bizRule='return !Yii::app()->user->isGuest;';
$auth->createRole('authenticated',$bizRule);
 
$bizRule='return Yii::app()->user->isGuest;';
$auth->createRole('guest',$bizRule);