验证和授权(Authentication and Authorization)

对于需要限制某些用户访问的网页,我们需要使用验证(Authentication)和授权(Authorization)。 验证是指核查一个人是否真的是他自己所声称的那个人。这通常需要一个用户名和密码, 但也包括任何其他可以表明身份的方式,例如一个智能卡,指纹等等。 授权则是找出已通过验证的用户是否允许操作特定的资源。 这一般是通过查询此用户是否属于一个有权访问该资源的角色来判断的。

Yii 有一个内置的验证/授权(auth)框架,用起来很方便,还能对其进行自定义,使其符合特殊的需求。

Yii auth 框架的核心是一个预定义的 用户(user)应用组件 它是一个实现了 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信息存成一个状态。

  1. 利用数据库来实现验证用户资格的authenticate()方法。
  2. 由于默认的getId方法直接返回username,所以需要重写CUserIdentity::getId()来返回真正的_id属性。
  3. 使用setState() (CBaseUserIdentity::setState)方法来手动将用户的其他信息保存到SESSION,以便后续操作使用。
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();

现在我们创建了一个新的UserIdentity对象而且把用户验证凭据(如:用户提交的$username$password值)传递给了它的构造函数。然后只需要简单地调用authenticate()方法。如果成功,我们就将身份信息传递给CwebUser::login方法,同时该方法会将身份信息保存到持久化存储中(默认是PHP的session),以供后续请求读取使用。如果验证失败,我们可以从errorMessage属性中得知失败原因。

无论一个用户是否经过鉴权,程序中都可以通过Yii::app()->user->isGuest来检查。如果采用session(默认)或cookie(下面会讨论)等持久化存储来保存身份信息,那么用户就可以在接下来的请求中保留登录信息。正因如此,我们不需要在接下来的每个请求中都使用UserIdentity类,更不需要再次走完整个登录流程。相反,CWebUser会根据Yii::app()->user->isGuest的返回值自动从持久化存储中读取身份信息。

基于Cookie 的登录

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

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

正如我们上面提到的,当基于cookie的登录开启后,通过CBaseUserIdentity::setState保存的状态将会同时保存到cookie中。当下次用户登录的时候,Yii::app()->user将会从cookie中读取这些状态来使用。

虽然Yii有一定措施来防止客户端的cookie被篡改,但我们还是强烈建议您不要在安全敏感的信息保存到用户状态(State)中。相反的,这些信息应该保存在服务器端(如:数据库中),并可以通过某些保存在服务端的持久化数据来读取。

除此之外,在一个正式的Web应用中,我们建议采用以下策略来增强基于Cookie登录的安全性。

  • 当一个用户成功通过填写用户登录表单登录之后,生成一个随机密钥,同时保存在cookie和持久化存储中(如:数据库)
  • 在接下来的请求中,当用户通过cookie信息验证时,我们通过比较两个密钥的一致性来确保用户正常登录。
  • 如果用户再次通过登录表单登录,重置密钥。

通过以上策略,我们可以排除用户使用可能包含过期信息的cookie登录的可能性。

要实现以上策略,我们需要重写以下两个方法:

  • CUserIdentity::authenticate():这是真正执行验证的地方。 如果用户验证成功,我们应该重新生成一个新的随机密钥,并使用CBaseUserIdentity::setState将它保存到用户状态中,同时保存到数据库中。
  • CWebUser::beforeLogin():当用户将要登入的时候调用。我们需要在此检查从cookie中获取的密钥是否和数据库中的一致。

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与其他较传统的访问控制模式的比较。

Yii 通过其 authManager 组件实现了分等级的 RBAC 结构。 在下文中,我们将首先介绍在此结构中用到的主要概念。然后讲解怎样定义用于授权的数据。在最后,我们看看如何利用这些授权数据执行访问检查。

概览(Overview)

在 Yii 的 RBAC 中,一个基本的概念是 授权项目(authorization item)。 一个授权项目就是一个做某件事的许可(例如新帖发布,用户管理)。根据其粒度和目标受众, 授权项目可分为 操作(operations)任务(tasks)角色(roles)。 一个角色由若干任务组成,一个任务由若干操作组成, 而一个操作就是一个许可,不可再分。 例如,我们有一个系统,它有一个 管理员 角色,它由 帖子管理用户管理 任务组成。 用户管理 任务可以包含 创建用户修改用户删除用户 操作组成。 为保持灵活性,Yii 还允许一个角色包含其他角色或操作,一个任务可以包含其他操作,一个操作可以包括其他操作。

授权项目是通过它的名字唯一识别的。

一个授权项目可能与一个 业务规则 关联。 业务规则是一段 PHP 代码,在进行涉及授权项目的访问检查时将会被执行。 仅在执行返回 true 时,用户才会被视为拥有此授权项目所代表的权限许可。 例如,当定义一个 updatePost(更新帖子) 操作时,我们可以添加一个检查当前用户 ID 是否与此帖子的作者 ID 相同的业务规则, 这样,只有作者自己才有更新帖子的权限。

通过授权项目,我们可以构建一个 授权等级体系 。在等级体系中,如果项目 A 由另外的项目 B 组成(或者说 A 继承了 B 所代表的权限),则 A 就是 B 的父项目。 一个授权项目可以有多个子项目,也可以有多个父项目。因此,授权等级体系是一个偏序图(partial-order graph)结构而不是一种树状结构。 在这种等级体系中,角色项目位于最顶层,操作项目位于最底层,而任务项目位于两者之间。

一旦有了授权等级体系,我们就可以将此体系中的角色分配给用户。 而一个用户一旦被赋予一个角色,他就会拥有此角色所代表的权限。 例如,如果我们赋予一个用户 管理员 的角色,他就会拥有管理员的权限,包括 帖子管理用户管理 (以及相应的操作,例如 创建用户)。

现在有趣的部分开始了,在一个控制器动作中,我们想检查当前用户是否可以删除指定的帖子。 利用 RBAC 等级体系和分配,可以很容易做到这一点。如下:

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

5. 配置授权管理器(Authorization Manager)

在我们准备定义一个授权等级体系并执行访问权限检查之前, 我们需要配置一下 authManager 应用组件。 Yii 提供了两种授权管理器: CPhpAuthManagerCDbAuthManager。前者将授权数据存储在一个 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 应用组件。

6. 定义授权等级体系

定义授权等级体总共分三步:定义授权项目,建立授权项目之间的关系,还要分配角色给用户。 authManager 应用组件提供了用于完成这三项任务的一系列 API 。

要定义一个授权项目,可调用下列方法之一,具体取决于项目的类型:

建立授权项目之后,我们就可以调用下列方法建立授权项目之间的关系:

最后,我们调用下列方法将角色分配给用户。

下面的代码演示了使用 Yii 提供的 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');

建立此授权等级体系后,authManager 组件(例如 CPhpAuthManager, CDbAuthManager) 就会自动加载授权项目。因此,我们只需要运行上述代码一次,并不需要在每个请求中都要运行。

信息: 上面的示例看起来比较冗长拖沓,它主要用于演示的目的。 开发者通常需要开发一些用于管理的用户界面,这样最终用户可以通过界面更直观地建立一个授权等级体系。

7. 使用业务规则

在定义授权等级体系时,我们可以将 业务规则 关联到一个角色,一个任务,或者一个操作。 我们也可以在为一个用户分配角色时关联一个业务规则。 一个业务规则就是一段 PHP 代码,在我们执行权限检查时被执行。 代码返回的值用来决定是否将角色或分配应用到当前用户。 在上面的例子中,我们把一条业务规则关联到了 updateOwnPost 任务。 在业务规则中,我们简单的检查了当前用户的 ID 是否与指定帖子的作者 ID 相同。 $params 数组中的帖子(post)信息由开发者在执行权限检查时提供。

权限检查

要执行权限检查,我们首先需要知道授权项目的名字。 例如,要检查当前用户是否可以创建帖子,我们需要检查他是否拥有 createPost 所表示的权限。 然后我们调用 CWebUser::checkAccess 执行权限检查:

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

如果授权规则关联了一条需要额外参数的业务规则,我们也可以传递给它。例如,要检查一个用户是否可以更新帖子, 我们可以通过 $params 传递帖子的数据:

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

使用默认角色

注意: 默认角色功能从 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', 'authenticated user', $bizRule);
 
$bizRule='return Yii::app()->user->isGuest;';
$auth->createRole('guest', 'guest user', $bizRule);
$Id: topics.auth.txt 2739 2010-12-14 01:50:04Z weizhuo $

Total 1 comment

#5690 report it
JoinPHP at 2011/11/01 11:07pm
貌似翻译跟不上

如何才能参加翻译?加入dev,然后google svn上面有吗

Leave a comment

Please to leave your comment.