应用Yii1.1和PHP5进行敏捷Web开发09

来源:互联网 发布:java applet实例 编辑:程序博客网 时间:2024/06/01 09:26

第八章:迭代5:用户访问控制

像我们前面制作的TrackStar应用程序这样基于用户的web应用程序中,通常需要针对谁对某一功能提出访问请求进行访问控制。我们所说的用户访问控制是指在一个较高的层次上,当访问请求被提出时一些需要被思考的问题,例如:

    • 请求的提出者是谁?
    • 提出请求的用户是否有足够的权限访问该功能?

上述问题的答案可以帮助应用程序做出适当的响应。

在上次迭代中完成的工作使得应用程序有能力提出第一个问题。我们的基础用户管理应用扩展了应用程序的用户认证流程,使之可以使用数据库内的资料。应用程序现在允许用户建立自己的认证信息,并且在用户登录时使用储存在数据库内的信息进行匹配。在用户成功登录后,应用程序就知道接下来的一系列请求是谁提出的。

本次迭代的核心任务是帮助应用程序回答第二个问题。一旦用户提供了足够的认证信息后,应用程序需要找到一种合适的方法去判断用户是否有足够的权限去执行要求的操作。我们将利用Yii的用户访问控制中的一些特性来扩展我们的基本授权模型。Yii即提供了一个简单的访问控制过滤器,也提供了一个更先进的RBAC(基于角色的访问控制)体系,来帮助我们完成授权需求。我们将在TrackStar应用程序中用户访问需求的实现中仔细观察这2 点。

迭代计划

当我们在第3章第一次介绍我们的TrackStar应用程序时,我们曾提及,这个应用程序拥有2个高级别用户类型:匿名用户和认证用户。这是一个小小的区别基于成功登录的用户和未登录的用户。我们也介绍过用户在同一个Project中扮演不同角色的想法。针对一个Project我们建立了如下的三类角色模型:

    • Project Owner(被赋予所有权限访问此Project的管理功能)
    • Project Member(被赋予一定权限访问此Project的特性和功能)
    • Project Reader (只有读取Project相关内容的权限,没有任何修改级权限)

这次迭代的重点是实施方法来管理这个应用程序中用户的访问控制权限。我们需要一个方法来建立我们的角色和权限,并且分发给用户,同时制定我们期望对每个角色施加的访问控制规则。

为了这个目标,我们需要标明所有我们将在本次迭代中完成的项目细节。下面的列表是这样一个项目列表:

    • 制定一个策略来强制用户在获得访问任何Project或Issue相关功能前,必须登录
    • 建立用户角色并且使之与一个特殊的功能权限对应
    • 制定一个为用户分配角色的机制(包含角色相关的权限)
    • 确保我们的角色权限结构针对每一个Project独立存在(也就是说,允许用户在不同的项目拥有不同的权限)
    • 制定一个让用户和项目,同时也和项目中的角色相关联的机制
    • 实施适当的认证访问检测,使得应用程序可以针对基于不同用户权限进行允许或拒绝访问操作

幸运的是,Yii内部有大量内部功能可以帮助我们完成这一需求。所以,让我们大干一场吧。

运行已存在的测试套件

如常,是时候运动一下了,我们需要运行一次所有已存在的单元测试,来确保测试可以通过:

% cd WebRoot/protected/tests/ % phpunit unit/ PHPUnit 3.4.12 by Sebastian Bergmann. ................ Time: 3 seconds OK (10 tests, 27 assertions)

一切都没有问题,所以我们可以开始本次旅程了。

访问控制过滤器

我们曾经在第三次迭代的时候介绍过过滤器,当时我们使用它来鉴别项目上下文关系,当处理Issue相关CRUD操作时。Yii框架提供的过滤器被叫做 accessControl(访问控制)。这个过滤器可以被直接使用在控制器类中,提供一种授权方案来验证用户是否可以使用一个特定的控制器下的行为。实际上,细心的读者应该能想起,我们在第六章使用filterProjectContext过滤器时,我们曾发现过,访问控制过滤器当时已经存在于 IssueController和ProjectController类的过滤器列表中,像下面的样子:

/*** @return array action filters*/ public function filters() {    return array(         'accessControl',        // perform access control for CRUD operations     );}

上面的代码包含在由Gii代码生成器在生成Issue和Project AR类的脚手架CRUD操作时,自动生成的代码里的。

默认的实现方法是设置为允许任何人观看一个已经存在Issue和Project项目的列表。然后,它只允许认证用户进行新建和更新操作,进一步限制帐号为 admin的用户享有删除行为。你也许还记得当我们第一次对Project实施CRUD操作时,我们必须登录才可以执行操作。同样的情形发生在对 Issues和Users的操作上。这种机械式的授权和访问模式就是由过滤器accessControl实现的。让我们仔细观察一下在 ProjectController.php文件中的这种实现方式。

有2个相关访问控制实现方法在这个文件中,ProjectController:filters()和ProjectController::accessRules()。第一个方法的代码如下:

/** * @return array action filters */public function filters() {    return array(         'accessControl', // perform access control for CRUD operations    );}

下面是第二个方法的代码:

/***Specifies the access control rules.*This method is used by the 'accessControl' filter.*@return array access control rules*/ public function accessRules(){    return array(        array('allow', // allow all users to perform 'index' and'view' actions            'actions'=>array('index','view'),            'users'=>array('*'),        ),         array('allow', // allow authenticated user to perform 'create' and 'update' actions            'actions'=>array('create','update'),            'users'=>array('@'),        ),         array('allow', // allow admin user to perform 'admin' and 'delete' actions            'actions'=>array('admin','delete'),             'users'=>array('admin'),        ),          array('deny', // deny all users            'users'=>array('*'),        ),    );}

filters()方法对我们来说已经非常熟悉了。我们在这里申明所有将在这个控制器类里使用的过滤器。在上面的代码中,我们只有一个 accessControl,引用了一个由Yii框架提供的过滤器。这个过滤器调用定义了如何驱动相关访问限制规则的accessRules()方法。

在前面提到的accessRules()方法中,有4条规则被申明。每一条都以数组形式存在。该数组的第一个元素是allow(通过)或deny(拒绝)。分别标识了获得或拒绝相关访问。剩下的部分由 name=>value键值对组成,申明了规则里剩余的参数。

让我们观察一下前面定义的第一条规则:

array('allow', // allow all users to perform 'index' and 'view' actions    'actions'=>array('index','view'),     'users'=>array('*'),),

这条规则允许任何用户执行控制器的index和view actions(行为)。星号‘*’特殊字符泛指所有用户(包括匿名,已认证,和其他类型)。

第二条规则被定义成如下形式:

array('allow', // allow authenticated user to perform 'create' and 'update' actions    'actions'=>array('create','update'),     'users'=>array('@'),),

这条规则允许认证用户(已登录)执行控制器的create和update actions。‘@’特殊字符泛指所有已认证用户。

下面是第三条规则:

array('allow', // allow admin user to perform 'admin' and 'delete' actions    'actions'=>array('admin','delete'),     'users'=>array('admin'),),

这里申明一个用户名为admin的特殊用户,被允许执行控制器的actionAdmin()和actionDelete()操作方法。

第四条规则如下定义:

array('deny', // deny all users     'users'=>array('*'),),

它拒绝了所有用户执行所有控制器的的所有action。

定义访问规则的时候可以使用一些 context parameters(内容参数)。前面提到的规则定义了行为和用户来组成规则的内容,下面是一个完整的参数列表:

    • Controllers(控制器):这条规则指定了一个包含多个控制器ID的数组,来指明哪些规则需要被应用。
    • Roles(角色):这条规则指定了一个将被规则使用的授权列表(包括角色,操作,权限等)。这些是为RBAC的一些功能服务的,我们将在下一个部分进行讨论。
    • Ips(IP地址):这条规则指定了一组可以被施加到规则的客户端IP地址。
    • Verbs(提交类型):这条规则制定了可以被施加到规则的HTTP请求类型。
    • Expression(表达式):这个规则指定了一个PHP表达式,这个表达式的值被用来决定这个规则是否应该被使用。
    • Actions(行为):这个规则指定了需要被规则匹配的对应action ID(行为ID)的方法。
    • Users(用户):这个规则指定了应该被施加规则的用户。登录当前项目的用户名作为被匹配项。3个特殊字符可以被使用:
      • *:任何用户
      • ?:匿名用户
      • @:登录用户/认证用户

访问规则按照其被申明的顺序一条一条的被评估。第一条规则与当前模型进行匹配,来判断授权结果。如果当前规则是一个allow的,这个行为(action)可以被执行;如果是一个deny规则,这个action无法被执行;如果没有规则和内容匹配,这个action仍然可以被执行。这是第四条被定义的原因。如果我们不在我们的认证列表末端定义一条deny全部用户全部action的规则,我们就无法完成预期的访问控制。拿第二个规则来举例,所有通过认证的用户可以执行create和update actions。但是它并不会拒绝匿名用户的请求。它对匿名用户熟视无睹。这里第四条规则确保所有和上面3条不匹配的请求都被拒绝掉。

当这些都完成后,修改我们的应用程序来拒绝匿名用户访问所有的Project,Issue和user相关的功能,绝对小菜一叠。我们需要做的是将用户数组的特殊字符’*’替换为’@’。这将只允许认证用户访问对应控制器的actionIndex()和actionView()方法。所有其他actions都拒绝认证用户访问。

对我们的控制器进行如下修改。打开下面3个文件:ProjectController.php, IssueController.php和userController.php,并且如下修改第一条访问控制规则:

array('allow', // allow only authenticated users to perform 'index' and 'view' actions    'actions'=>array('index','view'),     'users'=>array('@'),),

完成这些修改后,应用程序会在访问Project, Issue或User的任何功能前要求登录。我们依然允许匿名用户访问SiteController类的action方法,这是因为我们的登录方法存在于这个类中。我们必须允许匿名用户访问登录页。

基于角色的访问控制

现在我们已经使用了一个简单的访问控制过滤器(accessControl)来 broad stroke(泛泛的)限制授权用户访问,我们需要关注我们应用程序所需的更细节化的访问控制。如前所述,用户将在项目中扮演一定的角色。项目中将出现owner(主管)这样的用户类型,可以被视为项目管理员。他们将被赋予所有的项目管理权限。也会有member(成员)这样的用户类型,他们被赋予一部分项目功能权限,为主管权限的一个子集。最后还有一个reader(读者)用户类型,他们只有读没有任何修改的权限。为了完成这样的基于一个用户角色的访问模型,我们将探讨Yii的RBAC功能。

在管理已认证用户的访问许可的计算系统安全方面,RBAC是一条已经制定的标准。简单来说,RBAC标准中定义了一个应用程序中的角色。执行某些操作的权限也被定义,然后与角色相关联。然后用户将被分配到一个角色,并且通过这个角色获得与之相关的权限。对于有需要的读者,有大量关于RBAC概念和标准方面的文档可以阅读。其中一个优秀资源就是Wikipedia:http://en.wikipedia.org/wiki/Role-based_access_control。我们将专注与Yii的RBAC应用。

Yii中的RBAC应用是,简单、优雅、和强大的。Yii中RBAC的基础是一个叫授权项目的idea(想法)。授权项目就是一系列在应用程序中允许去做的事。这些允许可以被归类为roles(角色), tasks(任务), 或者operations(操作),因此形成权限层级。角色可有任务(或其他角色)组成,任务可以由操作(或其他任务组成),并且操作是最低权限级别。

例如,在我们的TrackStar应用程序中,我们需要一个owner的角色类型。所以我们建立一个角色类型名为owner的授权项目。这个角色将由类似user management(用户管理)和issue management(事务管理)的任务组成。这些任务进一步包含所需的原子操作。例如,用户管理任务可以由新建用户,编辑用户和删除用户等操作组成。权限等级是允许被继承的,来看个例子,如果一个用户被赋予owner角色,他就继承了新建,编辑,删除的操作权限。

一般来说,你赋予一个用户一定数量的角色,这用户就继承了属于这些角色的权限。这也是Yii中RBAC的真谛。当然,在这个列子中,我们可以将用户和任何授权项目相连接,而不是一类角色。这将允许我们灵活的将权限赋予任何级别的用户。如果我们只想赋予删除用户操作给一个特殊用户,而不是全部owner(主管)权限,我们可以将用户简单的与原子操作相连,这使得Yii中的RBAC相当灵活。

配置认证管理器

在我们建立授权等级,授予用户角色和访问权限检测之前,我们需要我们需要配置授权管理应用组件,authManager。这个组件用来存储权限信息,和管理权限与所提供的检测用户是否可以执行相关操作的方法之间的关系。Yii提供了2类授权管理器:CPhpAuthManager和 CDbAuthManager. CphpAuthManager使用PHP脚本文件储存授权信息。CDbAuthManager,正如你所猜的,使用数据库储存授权信息。authManager被配置为应用程序组件。只需要指定使用了2类的中哪一类,并且设置相关初始化类参数,就可完成授权管理器配置。

因为我们已经在TrackStar应用程序中使用了数据库,这使得,选择CDbAuthManager应用是更合理的。配置方法:打开位于protected/config/main.php的主配置文件,将下列代码添加至应用组件数组:

'authManager'=>array(     'class'=>'CDbAuthManager',     'connectionID'=>'db',),

这里建立了一个名为authManager的新应用组件,指定‘class’类型为CDbAuthManager,并且设置了类属性‘connectionID’为我们的数据库连接组件。至此,我们可以在整个应用程序的任何位置通过Yii::app()->authManager访问它。

建立RBAC使用的数据库表

如前所述,CDbAuthManager类使用数据库表来存储授权信息。它需要一个特定结构。结构文件位置:YiiRoot/framework/web/auth/schema.sql。这一简单而优雅的结构由3个表组成,AuthItem, AuthItemChild, 和AuthAssignment。表AuthItem用来储存授权项目的定义信息,包括是一个角色,任务或操作。表AuthItemChild用来储存形成授权项目层次的父子关系。最后,AuthAssignment表是用来存储用户和授权项目之间关系的关系表。基本的数据库定义语句如下:

create table AuthItem (     name    varchar(64) not null,    type    integer not null,    description    text,    bizrule    text,    data    text,    primary key (name)); create table AuthItemChild (    parent    varchar(64) not null,    child    varchar(64) not null,    primary key (parent,child),    foreign key (parent) references AuthItem (name) on delete cascade on update cascade,    foreign key (child) references AuthItem (name) on delete cascade on update cascade ); create table AuthAssignment (    itemname    varchar(64) not null,    userid    varchar(64) not null,    bizrule    text,    data    text,    primary key (itemname,userid),    foreign key (itemname) references AuthItem (name) on delete cascade on update cascade);

这个结构是直接从Yii框架文件/framework/web/auth/schema.sql中获取的,并未遵从我们的表命名规则。这些表名是CDbAuthManager类默认定义的。当然你可以自定义这些表名。简单起见,我们未做修改。

建立RBAC授权体系

将上述表添加到我们的_dev和_test数据库之后,我们需要将我们的角色和权限添加进去。我们将使用authManager提供的API来完成此操作。我们不会在此建立任何RBAC任务。下图展示了我们希望定义的基本等级关系:

images/book1/chapter8/1.jpg

图中的等级关系自上而下展示。所以Owner享有所有列表中的权限,包括从Member和Reader二个角色中继承来的。同样的Member继承了Reader的权限。现在我们需要做的是在应用程序中建立这一等级关系。入前面提到的,最佳方法是写代码调用authManager API。举个例子,下面的代码新建了一个角色和一个新操作,然后在角色和权限之间建立关系:

$auth=Yii::app()->authManager; $role=$auth->createRole('owner'); $auth->createOperation('createProject','create a new project'); $role->addChild('createProject');

在上面的代码中,我们首先创建了一个类authManager的实例,然后我们使用createRole(), createOperation, 和addChild()这些API接口方法来创建一个新owner角色和一个叫做createProject的新操作。然后我们为owner角色添加权限。这个创建示例只是我们需要的等级关系的一小部分,所有之前被列出的都应该以相似的方式创建。

为了完成构建我们所需的权限等级,我们要书写一段在命令行执行的简单的shell命令。这将是对我们用来创建最初应用程序的命令行工具yiic的一个扩展。

编写控制台应用程序命令

在第二章我们建立HelloWorld程序和第四章构建TrackStar应用程序骨架时我们介绍过yiic命令行工具。yiic是yii中以命令行形式执行任务的控制台应用程序。我们曾使用webapp命令来新建应用程序,并且在第二章我们还使用过yiic shell命令来新建一个controller(控制器)类。我们也曾用新的Gii代码生成工具来新建模型类和CRUD脚手架代码。这些工作也可以用yiic来完成。作为一个提示,yiic shell命令允许你通过命令行和web应用程序交互。你可以从包含应用程序脚本的文件包来执行它。同时,内部的一些特殊代码,提供了自动生成controllers(控制器), views(视图) and data models(数据模型)的功能.

Yii中的控制台应用程序可以轻松的通过编写自定义命令来扩充,而这正是我们要做的事。我们将通过编写新命令行工具来扩充yiic shell命令行工具集,这些新命令行工具允许我们使用一致和可重复的方式来构造我们的RBAC授权等级。

为控制台程序编写新命令是非常容易的。它是继承自CConsoleCommand类,以最低标准继承了所需的在命令被调用时执行的run()方法。类的名字应该与所期望的命令名字相同,并且在后面加上Command。在我们的案例中,命令的名字将简单的称为rbac,所以类的名字为RbacCommand。最后,为了使这个命令可以被yiic调用,我们需要将类文件保存至:/protected/commands/shell/文件夹。

所以,建立名为RbacCommand.php的新文件,并且添加如下代码:

<?php class RbacCommand extends CConsoleCommand{    private $_authManager;     public function getHelp()    {        return <<<EODUSAGE  rbacDESCRIPTION  This command generates an initial RBAC authorization hierarchy.EOD;    }     /**     * Execute the action.     * @param array command line parameters specific for this command     */    public function run($args)    {        //ensure that an authManager is defined as this is mandatory for creating an auth heirarchy        if(($this->_authManager=Yii::app()->authManager)===null)        {            echo "Error: an authorization manager, named 'authManager' must be configured to use this command.\n";            echo "If you already added 'authManager' component in application configuration,\n";            echo "please quit and re-enter the yiic shell.\n";            return;        }         //provide the opportunity for the use to abort the request        echo "This command will create three roles: Owner, Member, and Reader and the following premissions:\n";        echo "create, read, update and delete user\n"; echo "create, read, update and delete project\n";        echo "create, read, update and delete issue\n"; echo "Would you like to continue? [Yes|No] ";         //check the input from the user and continue if they indicated yes to the above question        if(!strncasecmp(trim(fgets(STDIN)),'y',1))        {            //first we need to remove all operations, roles, child relationship and assignments            $this->_authManager->clearAll();             //create the lowest level operations for users            $this->_authManager->createOperation("createUser","create a new user");            $this->_authManager->createOperation("readUser","read user profile information");            $this->_authManager->createOperation("updateUser","update a users information");            $this->_authManager->createOperation("deleteUser","remove a user from a project");             //create the lowest level operations for projects            $this->_authManager->createOperation("createProject","create a new project");            $this->_authManager->createOperation("readProject","read project information");            $this->_authManager->createOperation("updateProject","update project information");            $this->_authManager->createOperation("deleteProject","delete a project");             //create the lowest level operations for issues            $this->_authManager->createOperation("createIssue","crea te a new issue");            $this->_authManager->createOperation("readIssue","read issue information");            $this->_authManager->createOperation("updateIssue","upda te issue information");            $this->_authManager->createOperation("deleteIssue","dele te an issue from a project");             //create the reader role and add the appropriate permissions as children to this role            $role=$this->_authManager->createRole("reader");            $role->addChild("readUser");            $role->addChild("readProject");            $role->addChild("readIssue");             //create the member role, and add the appropriate permissions, as well as the reader role itself, as children            $role=$this->_authManager->createRole("member"); $role->addChild("reader");            $role->addChild("createIssue");            $role->addChild("updateIssue");            $role->addChild("deleteIssue");             //create the owner role, and add the appropriate permissions, as well as both the reader and             member roles as children            $role=$this->_authManager->createRole("owner");            $role->addChild("reader");            $role->addChild("member");            $role->addChild("createUser");            $role->addChild("updateUser");            $role->addChild("deleteUser");            $role->addChild("createProject");            $role->addChild("updateProject");            $role->addChild("deleteProject");             //provide a message indicating success             echo "Authorization hierarchy successfully generated.";        }    }}

上述代码中的注释可以很好的解释工作流程。我们添加了一个简单的getHelp()方法,可以帮助其他人快速上手。该命令也与其他yiic提供的命令保持一致。所有的一切都发生在run()方法内。该方法确保应用程序中有合法的authManager应用组件被定义。然后用户将进行最后的选择来决定是否执行。一旦得到用户的确认,它会清除所有RBAC表中的数据,然后写入新的权限等级资料。这里提到的等级正是我们前面提到的。

不难发现,即使基于如此简单的等级体系,仍然需要写大量的代码。一般的,人们需要制作一个更简单的用户界面来代替授权管理器API提供的简单的管理角色,任务和操作的接口。处于我们TrackStar应用程序的考虑,我们可以简单的设置数据库的表,之后运行一次逻辑代码建立基础逻辑关系,然后祈祷我们无需对此做过多修改。这是快速建立RBAC授权结构的一个不错的解决方案,但是并非一个长期解决方案。

在真实项目里,你可能需要一个拥有更多交互性的不同的工具来管理你的RBAC关系。Yii扩展库(http://www.yiiframework.com/extensions/)为之提供了一些打包好的解决方案。

让我们试一试新命令吧。cd到你的应用程序目录,然后执行shell命令(这里的YiiRoot代表你安装Yii框架的位置):

% YiiRoot/framework/yiic shell Yii Interactive Tool v1.1 (based on Yii v1.1.2) Please type 'help' for help. Type 'exit' to quit. >>

输入help,将显示所有可用命令列表:

At the prompt, you may enter a PHP statement or one of the following commands:  - controller  - crud  - form  - help  - model  - module  - rbacType 'help <command-name>' for details about a command.

在列表里可以看到我们的rbac命令。输入help rbac显示更多信息:

>> help rbacUSAGE    rbacDESCRIPTION    This command generates an initial RBAC authorization hierarchy.

上述显示了我们写在getHelp()方法中的信息。你可以随意对其进行扩充。

是时候启动命令建立所需的等级关系了。

>> rbacThis command will create three roles: Owner, Member, and Reader and the following premissions:create, read, update and delete usercreate, read, update and delete projectcreate, read, update and delete issueWould you like to continue? [Yes|No] YesAuthorization hierarchy successfully generated.

让我们退出shell:

>> exit

假设在提示继续时你输入Yes, 所有的权限等级关系都会被建立。

你可能还记得,我们为测试设置了一个叫trackstar_test的独立的数据库。因为我们也需要在测试数据库里建立这样的关系,我们将针对测试数据库运行yiic shell命令。因为测试数据库的连接字符串被定义在测试配置文件(/protected/config/test.php)中,我们需要对此文件使用yiic shell而不是main.php。这非常简单,因为yiic shell命令允许你指定不同的配置文件。让我们再次启动yiic shell,并且指向测试数据库:

% YiiRoot/framework/yiic shell protected/config/test.phpYii Interactive Tool v1.1 (based on Yii v1.1.2)Please type 'help' for help. Type 'exit' to quit.>> rbacThis command will create three roles: Owner, Member, and Reader and the following premissions:create, read, update and delete usercreate, read, update and delete projectcreate, read, update and delete issueWould you like to continue? [Yes|No] YesAuthorization hierarchy successfully generated.>> exit

至此,测试数据库中也存在了相应的RBAC授权层次结构。

为用户分配角色

目前我们所做的是建立了一个授权层次结构, 但并未给用户分配权限。通过给用户分配owner,member,或reader三个角色之一来完成分配权限的工作。例如,如果你想给独立用户ID为1的用户分配member角色,我们只需要如下操作:

$auth=Yii::app()->authManager; $auth->assign('member',1);

一旦建立了关系,检查权限就非常简单了。我们只需要询问应用程序的用户组件,当前用户是否有足够的权限。例如,我们想知道当前用户是否有新建Issue的权限,我们只需要遵循如下语法结构:

if( Yii::app()->user->checkAccess('createIssue')) {    //perform needed logic}

在这个例子中,我们给用于ID为1的用户授予member角色,并且在我们权限层次结构中,member角色继承到了createIssue权限,前面提到的if语句将会返回一个true,如果我们使用ID为1的用户登录。

在添加一个新用户到项目时我们将此授权逻辑作为业务逻辑代码的一部分来执行。我们将添加一个新的表单来帮助我们为项目添加用户,同时选择适当的角色。但在此之前,我们需要解决另一方面的问题:什么样的用户角色需要被应用到这个应用程序中,即他们需要基于不同的项目申请。

为项目添加RBAC角色

现在我们有了基础的RBAC授权模型,但没有将这些关系使用到整个应用程序中。在TrackStar应用程序中我们需要做的有一些复杂。我们需要针对项目定义角色,而不是针对整个应用程序的全局访问来定义角色。我们需要允许用户在不同项目中担任不同角色。举个例子,一个用户可能在一个项目中为reader 角色,在另外一个项目中为member角色,在第三个项目中为owner角色。用户可以存在于多个项目中,并且必须指明他们在每一个项目中的角色。

Yii内置的RBAC架构中没有任何符合我们要求的内置功能。RBAC模型倾向于建立角色和权限之间的关系。它对TrackStar项目一无所知,也不需要知道。为了在我们的授权层次结构中指定额外的维度,我们需要建立一张独立的数据表来维护用户,角色,项目之间的关系。以下为此表的数据库定义语句:

create table tbl_project_user_role (    project_id INTEGER NOT NULL,     user_id INTEGER NOT NULL,     role VARCHAR(64) NOT NULL,     primary key (project_id,user_id,role),     foreign key (project_id) references tbl_project (id),     foreign key (user_id) references tbl_user (id),     foreign key (role) references AuthItem (name));

所以打开你的数据库编辑器建立这张表,并且请确认它存在于主数据库和测试数据库之中。

添加RBAC业务逻辑

尽管之前的数据表将会保存一些基本信息来回答:一个用户是否被授予了一个特殊项目中的角色,但是仍然需要我们的授权层次结构来回答:用户是否有足够的权限来执行相关功能。虽然Yii中的RBAC模型并不知道我们的TrackStar项目,但是它有一些强悍的功能可以被利用。当你新建一个授权项目或赋予一个用户授权项目后,你可以适用一小段PHP代码(调用Yii::app()->user->checkAccess())进行检测。如果权限被授予了,上面的代码将在用户获得授权许可之前返回一个true。

对这一功能一个有用的例子是在应用程序中允许用户更新个人资料。一般情况下,应用程序需要确保用户只拥有更新自己资料的权限,而不是其他人的。所以我们需要建立一个叫updateProfie的授权项目,关联一定的业务逻辑来检查当前用户ID和需要更新资料的用户ID是否一致。

在我们的例子中,我们将为角色授予关联一个业务规则。当我们为用户授予一个角色时,我们会适用业务规则来检测项目中的关系。checkAccess()方法允许我们传递一个业务规则执行需要的附加数组。我们将适用这一功能传递至当前项目,这样业务规则可以调用Project AR类中的方法来判断当前用户是否被授予了当前项目中的角色。

我们将要创建的业务规则与每个角色的分配略有不同。例如,当我们给一个用户赋予owner角色时,将这样写:

$bizRule='return isset($params["project"]) && $params["project"]- >isUserInRole('owner');';

member和reader语句类似。

在调用checkAccess()方法后我们将可以访问项目内容。现在举一个例子检查用户访问createIssue权限,代码如下:

$params=array('project'=>$project); if(Yii::app()->user->checkAccess('createIssue',$params)) {    //proceed with issue creation logic}

这里的$project变量是带有当前项目内容的Project AR类实例(请明确,我们应用程序中的所有功能都出现在一个项目中)。

实现新Project AR类方法

我们已经添加了一张新数据表来存放用户,角色,项目之间的关系,现在我们需要实现用来的来管理和辨别表中数据的方法。我们添加公共方法到Project AR类来控制数据的添加和移除,同时用来完成辨别工作。如你所猜,我们会先写一个测试。

首先,让我们添加建立用户,项目和角色之间关系的功能。打开位于protected/tests/unit/ProjectTest.php的单元测试文件,添加如下测试:

public function testUserRoleAssignment() {    $project = $this->projects('project1');     $this->assertEquals(1,$project->associateUserToRole());}

然后运行测试:

% cd /Webroot/protected/tests/ % phpunit unit/ProjectTest.php PHPUnit 3.4.12 by Sebastian Bergmann. .....E Time: 0 seconds There was 1 error: 1) ProjectTest::testUserRoleAssignment CException: Project does not have a method named "associateUserToRole". ... FAILURES! Tests: 6, Assertions: 13, Errors: 1.

因为很明显的原因,测试无法通过。我们需要向Project AR类添加包含一个角色名、一个用户ID和已建立的基于角色、用户和项目之间关联的公共方法。打开位于protected/models /Project.php的文件,并且将下面有足够的逻辑可以让测试通过的代码添加进去:

/**   * creates an association between the project, the user and the user's role within the project   */public function associateUserToRole(){    return 1;}

再次运行,测试将返回成功的结果。

% phpunit unit/ProjectTest.php PHPUnit 3.4.12 by Sebastian Bergmann. ...... Time: 0 seconds OK (6 tests, 14 assertions)

现在让我们更新测试,传入角色名和用户ID:

public function testUserRoleAssignment() {    $project = $this->projects('project1');     $user = $this->users('user1');     $this->assertEquals(1,$project->associateUserToRole('owner', $user->id)); }

然后修改Project::associateUserToRole()方法,来加入这些参数,并在表tbl_project_user_role中插入一行:

public function associateUserToRole($role, $userId) {    $sql = "INSERT INTO tbl_project_user_role (project_id, user_id, role) VALUES (:projectId, :userId, :role)";    $command = Yii::app()->db->createCommand($sql);     $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);     $command->bindValue(":userId", $userId, PDO::PARAM_INT);     $command->bindValue(":role", $role, PDO::PARAM_STR);    return $command->execute();}

这里我们使用了Yii框架中的CDbCommand类针对数据库执行SQL语句。CDbCommand的一个实例返回自基于数据库连接的 createCommand()方法调用。我们还使用了CDbCommand类的bindValue()方法来绑定参数值。这是一个减少SQL注入攻击威胁的好方法,同时也帮助我们改进SQL语句多次执行的性能。

CDbCommand::execute()方法一般在执行insert语句前返回受影响的行数。一次成功的insert将会影响1行,所以整数1将被返回。测试将执行的返回结果同1做比较。如果你一直跟随我们的学习,测试将会通过。但是当你第二次运行,将会发现数据库完整性约束违规的失败,因为它将尝试 2次插入相同的主键。我们需要花费一些时间来解决这一问题。

当我们在测试中处理数据表时,我们应当已经为该表添加了夹具,使得以可重复和一致性的方式运行我们的测试。

在fixtures文件夹(protected/tests/fixtures/)添加一个叫tbl_project_user_role.php的新文件,我们只需简单的返回一个空数组:

<?php return array( );

接下来,修改位于protected/tests/unit/Project-Test.php中的fixtures array,使之包含新的夹具:

public $fixtures=array(     'projects'=>'Project',    'users'=>'User',     'projUsrAssign'=>':tbl_project_user_assignment',     'projUserRole'=>':tbl_project_user_role', );

即使我们并未为我们的夹具添加任何实际数据,夹具管理器将清空我们的tbl_project_user_role表,也就是说在每次测试前移除所有插入的行。现在我们可以多次运行我们的测试而不用担心任何数据库错误。

当我们改变用户在项目中的角色,或者从一个项目中移除用户时,我们需要移除这样的关联。因此,让我们为此添加一个方法。我们可以使用同一个测试。

让我们修改我们的测试,添加一个调用来移除关联,在添加的后面:

public function testUserRoleAssignment() {    $project = $this->projects('project1');     $user = $this->users('user1');     $this->assertEquals(1,$project->associateUserToRole('owner', $user->id));    $this->assertEquals(1,$project->removeUserFromRole('owner', $user->id));}

再次运行测试,你将看到失败。我们需要在Project AR类中实现这个新方法。在类的尾部添加下面的方法:

/**  * removes an association between the project, the user and the user's role within the project  */ public function removeUserFromRole($role, $userId) {    $sql = "DELETE FROM tbl_project_user_role WHERE project_ id=:projectId AND user_id=:userId AND role=:role";    $command = Yii::app()->db->createCommand($sql);     $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);     $command->bindValue(":userId", $userId, PDO::PARAM_INT);     $command->bindValue(":role", $role, PDO::PARAM_STR);    return $command->execute();}

只是简单的从数据库中删除了角色,用户和项目之间的关联行。如果成功删除将返回影响的行数为整数1。至此,我们的测试添加并删除了新关联。我们需要再次运行确保一切正常。

% phpunit unit/ProjectTest.php PHPUnit 3.4.12 by Sebastian Bergmann. ...... Time: 1 second OK (6 tests, 15 assertions)

我们已经实现了添加和删除关联的方法。现在我们需要添加函数来判断一个给出的用户是否与项目中的角色有关系。为此我们还需要向Project AR类添加一个公共方法。

所以,作为一个测试,添加如下的测试方法到ProjectTest.php:

public function testIsInRole(){    $project = $this->projects('project1');     $this->assertTrue($project->isUserInRole('member'));}

这个被设计用来测试Project::isUserInRole()方法的实现。因为我们并未事件这一方法,测试显然会失败。确保结果如下:

% phpunit unit/ProjectTest.php PHPUnit 3.4.12 by Sebastian Bergmann. ......E Time: 0 seconds There was 1 error:1) ProjectTest::testIsInRole CException: Project does not have a method named "isUserInRole". ... FAILURES! Tests: 7, Assertions: 15, Errors: 1.

使它通过,需添加如下方法到Project AR类的底部:

/** * @return boolean whether or not the current user is in the specified role within the context of this project */ public function isUserInRole($role) {    return true;}

这已经足够使得测试通过了。

% phpunit unit/ProjectTest.php ... OK (7 tests, 16 assertions)

下面我们需要实现足够的逻辑来确定是否存在一定的关联。修改Project AR类中的关系:

public function isUserInRole($role) {    $sql = "SELECT role FROM tbl_project_user_role WHERE project_id=:projectId AND user_id=:userId AND role=:role";    $command = Yii::app()->db->createCommand($sql);     $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);     $command->bindValue(":userId", Yii::app()->user->getId(), PDO::PARAM_INT);     $command->bindValue(":role", $role, PDO::PARAM_STR);     return $command->execute()==1 ? true : false;}

这里再次执行SQL语句直接从我们表中读取。它需要输入角色名和当前用户信息(Yii::app()->user),来组成搜索所需的主键。再次运行测试:

% phpunit unit/ProjectTest.php ... Time: 1 second, Memory: 14.25Mb There was 1 failure:1) ProjectTest::testIsInRole Failed asserting that <boolean:false> is true. ... FAILURES! Tests: 7, Assertions: 16, Failures: 1.

测试再次失败。失败的原因是isUserInRole()方法使用了Yii::app()->user->getId()来获得当前用户 ID,但实际上不会返回任何东西。我们的测试并不会在调用前直接设置当前用户。让我们编辑逻辑代码来设置当前用户ID。这样修改测试方法:

public function testIsInRole() {    $user = $this->users('user1');     Yii::app()->user->setId($user->id);     $project = $this->projects('project1');     $this->assertTrue($project->isUserInRole('member'));}

这里设置了当前用户ID为用户夹具信息里user1的。让我们再次运行测试:

% phpunit unit/ProjectTest.php ... Time: 1 second, Memory: 14.25Mb There was 1 failure:1) ProjectTest::testIsInRole Failed asserting that <boolean:false> is true. ... FAILURES! Tests: 7, Assertions: 16, Failures: 1.

我们的测试依然是失败的,这是因为表中并不存在相关行,user1并不是此项目的owner。所以让我们在调用isUserInRole()方法之前先建立关联。

我们可以使用我们之前在测试中建立的添加和移除这些关联的其他方法来建立这样的关系。然而,为了试图保持这个测试的独立于其他测试或Project AR类中的方法,我们将依靠夹具数据来提供初始化条件。

在我们初次添加夹具文件(tests/fixtures/tbl_project_user_role.php)时,我们简单的让其返回一个空数组。作一些修改让其提供一行ProjectID为2,用户ID为2,角色为member的信息:

return array(     'row1'=>array(        'project_id' => 2,         'user_id' => 2,         'role' => 'member',    ),);

有了之前的添加和移除使用user1和project1为夹具数据的测试经验,我们可以避免不同ID数据的冲突。

现在我们将使用夹具数据来设置建立Project AR类时的用户ID。如下修改测试方法:

public function testIsInRole() {    $row1 = $this->projUserRole['row1'];     Yii::app()->user->setId($row1['user_id']);     $project=Project::model()->findByPk($row1['project_id']);    $this->assertTrue($project->isUserInRole('member'));}

这里我们使用了在类的顶部定义的叫projUserRole的夹具数据来取回针对行数据。然后我们使用这个数据来设置用户ID,同时通过调用 Project::model()->findByPK创建Project AR类实例。下面我们通过测试来确保用户被授予了member角色。如果我们运行测试:

% phpunit unit/ProjectTest.php ... OK (7 tests, 16 assertions)

我们的测试再次通过。

我们写并测试添加和移除一个项目中管理的角色,并且通过一个方法来判断一个给定的用户是否与一个项目中的角色相关联。下面我们将写一个最终测试。我们准备写一个端对端应用的测试,来检测我们准备为这个项目添加的而外Yii的RBAC结构维度。我们所说实现这一目标是通过添加业务规则到Yii RBAC授权(每当我们将用户和角色关联起来的时候)中。让我们写一个最终方法来测试这个:

打开ProjectTest.php单元测试文件,添加如下测试方法:

public function testUserAccessBasedOnProjectRole() {    $row1 = $this->projUserRole['row1'];    Yii::app()->user->setId($row1['user_id']);    $project=Project::model()->findByPk($row1['project_id']);     $auth = Yii::app()->authManager;    $bizRule='return isset($params["project"]) && $params["project"]->isUserInRole("member");';    $auth->assign('member',$row1['user_id'], $bizRule);     $params=array('project'=>$project);     $this->assertTrue(Yii::app()->user->checkAccess('updateIssue', $params));    $this->assertTrue(Yii::app()->user->checkAccess('readIssue',$ params));    $this->assertFalse(Yii::app()->user->checkAccess('updateProje ct',$params));}

最终测试方法使用了其他的已经存在并且通过测试的API方法来构成,所以就不需要经历我们一般的TDD步骤了。有时,这一点会引起争论,比起单元测试更像功能测试,但我们还是将其放入单元测试类。

我们将使用相同的方式(如同我们之间测试的)来设置用户ID和通过来自tbl_project_user_role.php夹具文件中的数据来建立 Project AR类实例。随后我们建立了auth管理类的实例来授予用户owner角色。然而,在我们执行授权操作之前,我们创建了业务规则。这条业务规则使用$params数组,首先检查是否存在数组中project元素,然后调用Project AR类的isUserInRole()方法(该方法假设值是数组中的元素)。我们直接传递名称owner给这个方法,因为这个角色是我们已经赋予的。最终我们调用了Yii RBAC相关方法Yii::app()->user->checkAccess()来确定当前用户是否被授予了基于此项目的我们授权层次结构的owner角色。

我们检测了用户是否有权限更新issue,member以上的角色拥有这一权限。我们期望返回一个true。我们也制作了一部分断言来测试(和证明)权限的继承。我们期望一个member角色可以继承reader的权限。所以我们也测试了一个在我们授权层次结构中为reader后代角色的用户拥有 readIssue权限。最后,我们希望拒绝owner角色的操作。所以,我们使用测试确保对权限updateProject返回false。

再次运行测试:

% phpunit unit/ProjectTest.php ... OK (8 tests, 19 assertions)

所有的项目测试都通过了。似乎这个方法可以了。

如果我们直接使用代码:

$auth->assign('member',$row1['user_id'], $bizRule);

向AuthAssignment表插入一行数据,如果我们再次运行测试,我们将会获得一个数据库完整性冲突的警告。简单的说它将试着重复插入相同行,并且会违反我们之前在表中定义的数据完整性约束。为了避免这个,我们需要让夹具管理器也可以控制这张表。与之前一样。在位于protected/tests/fixtures/的夹具文件夹下建立AuthAssignment.php文件,使之返回一个空数组。然后修改位于ProjectTest.php文件顶部的夹具数组,使之包含新建文件:

public $fixtures=array(     'projects'=>'Project',    'users'=>'User',     'projUsrAssign'=>':tbl_project_user_assignment',     'projUserRole'=>':tbl_project_user_role',     'authAssign'=>':AuthAssignment',);

现在我们的AuthAssignment表都会在每次测试前被重置。

在我们完成测试之前,让我们再添加一点来确保项目中没有授权的用户,没有任何权限。因为我们直接设置的关系是基于项目id等于2的,让我们使用项目id等于1的来测试用户访问权限。在testUserAccessBasedOnProjectRole()方法尾部加上下面的代码:

//now ensure the user does not have any access to a project they are not associated with$project=Project::model()->findByPk(1); $params=array('project'=>$project); $this->assertFalse(Yii::app()->user->checkAccess('updateIssue', $params)); $this->assertFalse(Yii::app()->user->checkAccess('readIssue', $params)); $this->assertFalse(Yii::app()->user->checkAccess('updateProject', $params));

在这里我们建立了一个基于project_id=1的项目实例。因为我们知道用户被没有与该项目关联,所以所有的checkAccess()调用都会返回false。

添加用户到项目

在上一次迭代中,我们添加了为应用程序创建新用户的功能。但是我们并没有方法将用户分配到项目,更深一层,分配用户到项目中的角色。现在我们完成了我们的RBAC练习,是时候完成这一新功能了。

所需功能的实现包括了几处代码的改变。然而,我们提供了相似的需要改变类型的例子,并且在实现之前的迭代的功能中,包含了所有相关的概念。所以,在这一步我们会比较快,并且只停下来强调几件我们没有提到的事情。介于这样,读者需要在没有帮助的情况下完成改变,并且这是一个亲手练习的好机会。为了进一步鼓励这样的练习,我们将首先列出完成新需求所需要作的所有事。你可以合上书,自己试着做一些,然后再看下面提供的实现方法。

达成目标所需要做的事:

    • 使用测试优先的方法,为Project模型类添加一个叫getUserRoleOptions()的public static方法,用来返回一列正确的取自auth manager的getRoles()方法的可选角色。在添加用户到项目时我们将使用这一列表来填充一个可选角色下拉菜单。
    • 使用测试优先的方法,为Project模型类添加一个叫associateUserToProject($user)的public方法,来关联用户到项目。这一方法将直接通过插入数据到tbl_project_use_assignment表的方式来建立用户和项目之间的关联。
    • 使用测试优先的方法,为Project模型类添加一个叫isUserInProject($user)的public方法,来判断用户是否已经与项目关联。我们将在验证提交的规则中使用它,来防止尝试将重复的用户添加到同一个项目中。
    • 添加一个叫ProjectUserForm新表单模型类,扩展自CFormModel作为一个新的输入表单模型。为此表单模型类添加3个属性:$username,$role,和$project.同时添加验证规则来确保username和role必须被填写,然后username迟一些将被类自定义方法 verify()验证。verify()方法应该是这样的:
      • 试着用匹配输入的用户名来创建一个User AR类的实例
      • 如果尝试成功了,将用新方法associateUserToProject($user)将用户关联到一个项目上,此方法是在本章早期将用户和角色关联时添加的。如果没有与用户名匹配的用户被找到,需要设置返回一个错误。(如果需要,看一下LoginForm::authenticate()如何进行自定义验证的)
    • 在views/project目录建立一个叫adduser.php的新view文件,来显示我们的新添加用户到项的表单。这个表单只需要2个输入:username和由下拉菜单提供的role
    • 在ProjectController类下添加一个叫actionAdduser()的新控制器行为方法,然后修改此控制器的accessRules()方法来确保一个已认证的会员发起访问。新的方法用来负责渲染新view来显示表单,并且处理表单提交的数据。

再提醒一下,我们非常鼓励读者尝试自己完成上述修改。我们在下面的部分列出了修改的代码。

修改Project模型类

我们为Project类添加了3个新public方法,其中一个为static,因此可以在非实例化的情况下调用:

/**   * Returns an array of available roles in which a user can be placed when being added to a project   */public static function getUserRoleOptions() {     return CHtml::listData(Yii::app()->authManager->getRoles(), 'name', 'name'); } /**   * Makes an association between a user and a the project   */public function associateUserToProject($user) {    $sql = "INSERT INTO tbl_project_user_assignment (project_id, user_id) VALUES (:projectId, :userId)";    $command = Yii::app()->db->createCommand($sql);    $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);     $command->bindValue(":userId", $user->id, PDO::PARAM_INT);     return $command->execute();}  /**  * Determines whether or not a user is already part of a project  */ public function isUserInProject($user) {    $sql = "SELECT user_id FROM tbl_project_user_assignment WHERE project_id=:projectId AND user_id=:userId";    $command = Yii::app()->db->createCommand($sql);     $command->bindValue(":projectId", $this->id, PDO::PARAM_INT);     $command->bindValue(":userId", $user->id, PDO::PARAM_INT);     return $command->execute()==1 ? true : false;}

对于上面的代码没有任何特殊说明。因为它们都是Project模型类的public方法,我们ProjectTest单元测试类中添加了下面2个测试方法作为结束:

public function testGetUserRoleOptions() {    $options = Project::getUserRoleOptions();    $this->assertEquals(count($options),3);    $this->assertTrue(isset($options['reader']));    $this->assertTrue(isset($options['member']));    $this->assertTrue(isset($options['owner']));} public function testUserProjectAssignment() {    //since our fixture data already has the two users     //assigned to project 1, we'll assign user 1 to project 2    $this->projects('project2')->associateUserToProject($this- >users('user1'));    $this->assertTrue($this->projects('project1')->isUserInProject($this->users('user1')));}

添加新表单模型类

如果在login from中做的练习一样,我们试着建立一个新的表单模型类用来存放表单的输入和进行集中验证。这是一个非常简单的继承自Yii CFormModel类的类,它的属性和我们的输入框匹配,也有一个来存放正确的项目信息。我们需要利用这个项目信息来添加用户到项目。整个类的代码如下:

<?php /**  * ProjectUserForm class.   * ProjectUserForm is the data structure for keeping   * the form data related to adding an existing user to a project. It is used by the 'Adduser' action of 'ProjectController'.   */class ProjectUserForm extends CFormModel {    /**       * @var string username of the user being added to the project       */    public $username;     /**       * @var string the role to which the user will be associated within the project       */    public $role;     /**       * @var object an instance of the Project AR model class      */    public $project;     /**       * Declares the validation rules.       * The rules state that username and password are required,       * and password needs to be authenticated using the verify() method       */    public function rules()     {        return array(             // username and password are required             array('username, role', 'required'),            // password needs to be authenticated             //array('username', 'verify'),             array('username', 'exist', 'className'=>'User'),             array('username', 'verify'),        );    }        /**       * Authenticates the existence of the user in the system.       * If valid, it will also make the association between the user,      * role and project * This is the 'verify' validator as declared in rules().       */    public function verify($attribute,$params)     {        if(!$this->hasErrors()) // we only want to authenticate when no other input errors are present        {            $user = User::model()->findByAttributes(array('username'=> $this->username));            if($this->project->isUserInProject($user)) {                $this->addError('username','This user has already been added to the project.');            } else {                $this->project->associateUserToProject($user);                 $this->project->associateUserToRole($this->role, $user->id);                $auth = Yii::app()->authManager;                $bizRule='return isset($params["project"]) && $params["project"]->isUserInRole("'.$this->role.'");';                $auth->assign($this->role,$user->id, $bizRule);            }        }    }}

添加新action方法到Project Controller

我们需要一个控制器action来处理初始化请求呈现给用户添加用户到项目的表单。我们将这些添加至ProjectController类,并且命名为actionAdduser()。代码如下:

public function actionAdduser() {    $form=new ProjectUserForm;     $project = $this->loadModel();    // collect user input data    if(isset($_POST['ProjectUserForm'])) {        $form->attributes=$_POST['ProjectUserForm'];         $form->project = $project; // validate user input and set a sucessfull flassh message if valid        if($form->validate())         {            Yii::app()->user->setFlash('succe


原创粉丝点击