+ -
当前位置:首页 → 问答吧 → FleaPHP 开发基础 - 第三部分

FleaPHP 开发基础 - 第三部分

时间:2009-04-20

来源:互联网

七、数据表关联

数据表关联是指两个或者多个数据表的记录之间的逻辑关系。
例如:
每一个公民都有一个身份证号码每一位作者都写了多本(0-n)书籍,而每一本书籍都有多个(1-n)作者每一篇文章都有多个(0-n)评论每一个评论都属于一篇文章目前,FleaPHP 支持四种类型的数据表关联,分别是:
HAS_ONE: 当前表的每一条记录都拥有最多一条(0–1)关联记录HAS_MANY: 当前表的每一条记录都拥有多条(0-n)关联记录MANY_TO_MANY: 当前表的每一条记录都和其他表的多条(0-n)记录关联BELONGS_TO: 当前表的每一条记录都属于另一个表的某条记录在 FleaPHP 中,可以为每一个表数据入口定义多个不同的关联,例如:

术语在详细介绍这四种关联之前,先了解一些后文将会用到的术语。
关联: 一个关联是一个关系,这个关系属于某一个数据表。例如 users 表可能就拥有一个或者多个关联。主表: 对于一个关联,拥有该关联的数据表就是主表。例如 posts 表定义了一个 MANY_TO_MANY 关联。那么在这里(指这个关联),posts 就是主表。关联表: 在一个关联中,关联表就是除主表外的另一个表。外键: 在数据库原理中,外键的含义很复杂。但在 FleaPHP 框架中的数据库关联功能中,外键泛指一个记录中用于关联另一个记录的字段。例如 profile 表中的 user_id 字段就是用于关联 users 表的字段。这个 user_id 字段就是一个外键。中间表: 在 MANY_TO_MANY 关联中,除了主表和关联表,还需要另一个表来保存这两个表的记录之间的互相关联关系。这个表称为中间表。理解这几个术语后,我们再来看每一种关联的详细解释。

HAS_ONE 一对一关联HAS_ONE 是一种非常简单的关联关系。表示一个记录拥有另一个记录。这两个记录分别位于两个数据表中。

示例在一个信息管理系统中,users 表用于存储用户帐户的基本信息,例如用户名、密码等。而 profiles 表则用于存储用户的个人信息,例如家庭住址、邮政编码等。
由于每一个用户(一条 users 表中的记录)都有一份对应的个人信息(一条 profiles 表中的记录)。因此,我们就可以为 users 表定义一个 HAS_ONE 关联。
很明显,users 表的记录拥有一条 profiles 表的记录。因此,当 users 表中的一条记录被删除时,被删除记录所拥有的 profiles 表中的关联记录也会被自动删除。

表定义在 HAS_ONE 关联中,要求外键放置在关联表中。
上述例子的表定义简化版如下:
users 表:
user_id 主键字段usernameprofiles 表:
profile_id 主键字段addresspostcodeuser_id 外键字段对应的 MySQL 代码如下:
CREATE TABLE `users` ( `user_id` INT NOT NULL AUTO_INCREMENT , `username` VARCHAR( 32 ) NOT NULL , PRIMARY KEY ( `user_id` ));CREATE TABLE `profiles` ( `profile_id` INT NOT NULL AUTO_INCREMENT , `address` VARCHAR( 128 ) NOT NULL , `postcode` VARCHAR( 8 ) NOT NULL , `user_id` INT NOT NULL , PRIMARY KEY ( `profile_id` ));对应的 FLEA_Db_TableDataGateway 继承类的定义如下:

演示代码结果很有趣,多出来的 ‘profile’ 字段正好是我们刚刚插入 profiles 表的记录内容:
Array( [user_id] => 1 [username] => dualface [ref___id] => 1 [profile] => Array ( [profile_id] => 1 [address] => SiChuan ZiGong [postcode] => 643000 [user_id] => 1 [ref___id] => 1 ))
说明在上面的例子中,Users 类中有一个 $hasOne 成员变量。该变量为一个数组:
var $hasOne = array( 'tableClass' => 'Profiles', 'foreignKey' => 'user_id', 'mappingName' => 'profile',);$hasOne 成员变量用于为一个表数据库入口指定 HAS_ONE 关联。
在关联的定义中,tableClass 指定关联表的表数据入口类名称,foreignKey 指定外键字段名,而 mappingName 则指定在主表的查询结果中用什么字段映射关联表的数据。


八、如何使用 RBAC 组件实现访问控制

如何使用 RBAC 组件?
请务必更新到 1.0.60.553 版本以上,确保 RBAC 组件与文档中的描述相符合
RBAC 是英文(Role-Based Access Control)的缩写,也就是基于角色的访问控制。RBAC 的定义比较晦涩,我就以比较生动的形式来阐述什么是 RBAC。

ATM 机的一天假设有一台 ATM(自动提款机)放在街边,我们来看看这个 ATM 度过的一天。
早上,有一个家伙走到 ATM 面前,对着机器说:“芝麻开门,芝麻开门,给我 100 块!”。很显然 ATM 不会有任何动作。失望之余,这个家伙踢了 ATM 一脚走了。中午,一位漂亮的 Office lady 走到 ATM 机面前,放入她的信用卡,输入密码后,取出了 1200 块钱。当然,这些钱很快就会变成一件衣服或是化妆品。下班时分,银行的工作人员来到 ATM 机器面前,放入一张特制的磁卡,然后输入密码。从中查询到 ATM 机器内还有充足的现金,无需补充。所以他很高兴的开着车去下一台 ATM 机器所在地了。现在我们要开发一台具有同样功能的 ATM 机,应该怎么做呢?
首先,我们的 ATM 机不能让人随便取钱,不然银行会破产的。接下来,ATM 机需要一个让人们放入磁卡并输入密码的设备。人们放入磁卡并输入密码后,ATM 机还要能够判断这张磁卡的卡号和密码是否有效,并且匹配。之后,ATM 机必须判断磁卡的卡号属于哪种类型,如果是信用卡,那么则显示查询账户余额和取款的界面。如果是特制的磁卡,则显示 ATM 机内的现金余额。

ATM 与 RBAC上面的例子显得有点荒诞,但是却是一个典型的基于角色的访问控制。
对于没有磁卡或者输入了错误密码的用户,一律拒绝服务,也就是不允许进行任何其他操作;如果输入了正确的密码,必须判断用户输入哪一种类型,并提供相应的服务界面;如果用户尝试访问自己不能使用的服务,那么要明确告诉用户这是不可能的。这个流程中,一共出现了两种角色:信用卡用户和管理卡用户。而那些没有磁卡的用户,都属于没有角色一类。RBAC 要能够工作,至少需要两个数据:角色信息和访问控制表。
角色信息通常是指某个用户具有的角色,例如你持有一张信用卡,那么你就具有“信用卡用户”这个角色。如果你持有一张管理卡,那么你就具有“管理卡用户”这个角色。如果你既没有信用卡,又没有管理卡,那么你就没有上述两种角色。
有了角色信息,RBAC 系统还需要一个访问控制表。访问控制表(Access Control Table)是一组数据,用于指出哪些角色可以使用哪个功能,哪些角色不能使用哪个功能。例如在 ATM 机中,具有“信用卡用户”角色,就可以使用查询账户余额和取款两项功能;而具有“管理卡用户”角色,就可以使用查询 ATM 机内现金余额的动能。
我们来模拟一次 ATM 机的操作:
唐雷有一张信用卡,他放入 ATM 机并输入了正确的密码。这时,他被 ATM 机认为具有“信用卡用户”角色。根据上面的判断结果,ATM 机显示了一个操作界面,上面有查询账户余额和取款两项操作按钮。唐雷按下了“查询账户余额”按钮,ATM 机的查询账户余额功能被调用。在查询账户余额功能中,再次检查用户的角色信息,确定他可以使用这个功能。进行一系列操作,然后将唐雷信用卡账户上的余额数字显示到屏幕上。唐雷很郁闷他的信用卡又透支了,悻悻然取出卡走人了。这时 ATM 自动清除当前的角色信息,为下一次操作做好准备。从上面可以看出,RBAC 充当了系统的一道安全屏障。所有的操作都需要进过 RBAC 验证过后才能使用。这样充分保证了系统的安全性。

RBAC 概念在 FleaPHP 的 RBAC 组件中,只有下列几项概念需要理解:
用户:应用程序的使用者;角色:一个名字,可以为用户指定多个角色(0-n);访问控制表(ACT):一个数组,用来指明哪些功能可以被哪些角色访问或者限制访问。除了上述三个概念,要想 RBAC 系统能够正常工作,还需要用户信息管理器、角色信息管理器和访问控制器三个部件。
用户信息管理器:提供用户信息的存储、查询服务,以及为用户指定角色信息;角色信息管理器:提供角色信息的存储和查询服务访问控制器:根据角色信息和访问控制表进行验证FleaPHP 中已经实现了上述三个部件,所以开发者要做的功能就比较简单了。

使用 RBACFleaPHP 中提供了 FLEA_Com_RBAC、FLEA_Com_RBAC_UsersManager 和 FLEA_Com_RBAC_RolesManager 三个部件,以及 FLEA_Dispatcher_Auth 调度器。
其中,FLEA_Com_RBAC_UsersManager 提供用户信息存储服务,而 FLEA_Com_RBAC_RolesManager 提供角色信息存储服务。FLEA_Com_RBAC 则和 FLEA_Dispatcher_Auth 结合,一起提供了访问控制能力。
下面我们来看看 RBAC 到底怎么工作的。

修改应用程序设置要使用访问控制功能,首先需要修改应用程序设置。让应用程序使用 FLEA_Dispatcher_Auth 调度器,而不是默认的 FLEA_Dispatcher_Simple 调度器。
FLEA_Dispatcher_Auth 调度器和 FLEA_Dispatcher_Simple 调度器的基本功能一样。但在调用控制器动作方法前,FLEA_Dispatcher_Auth 调度器会通过 FLEA_Com_RBAC 组件获取保存在 session 中的用户角色信息,然后再读取控制器的访问控制表(ACT)。最后调用 FLEA_Com_RBAC::check() 方法检查用户拥有的角色是否可以访问这个控制器及要调用的控制器动作。
验证通过,则控制器动作方法会被调用,否则将显示错误信息,或者调用应用程序设置 dispatcherAuthFailedCallback 指定的错误处理程序。

准备控制器的 ACT 文件设置好应用程序后,接下来要做的就是为控制器准备 ACT 文件。
ACT 文件和控制器文件同名,并且保存在同一个目录下,只是扩展名为 .act.php。例如控制器 Controller_Default 的文件名是 Controller/Default.php,那么该控制器的 ACT 文件名就是 Controller/Default.act.php。
ACT 文件的内容通常使用下面的格式:
可以看到,ACT 文件只是单纯的返回一个数组。这个数组遵循下面的格式:
array( 'allow' => '允许访问该控制器的角色名', 'deny' => '禁止访问该控制器的角色名', 'actions' => array( '动作名' => array( 'allow' => '允许访问该动作的角色名', 'deny' => '禁止访问该动作的角色名', ), // .... 更多动作 ),);在上面的格式中,角色名可以是多个,例如“POWER_USER, MANAGER”。只需要用“,”分隔多个角色名就可以了。
通常,我们只需要为控制器指定 allow 或者 deny 就可以了。但有时候我们要允许多个角色都可以访问该控制器,但该控制器中的特定方法只允许上述角色中部分角色可以访问。这时,我们可以通过'动作名' => array('allow' => '角色名', 'deny' => '角色名') 的方式来指定该控制器动作特有的 ACT。
为了便于开发,FleaPHP 预定义了几个角色,分别是:
RBAC_EVERYONE:表示任何用户(不管该用户是否具有角色信息)RBAC_HAS_ROLE:表示具有任何角色的用户(该用户必须有角色信息)RBAC_NO_ROLE:表示不具有任何角色的用户RBAC_NULL:表示该设置没有值 特别注意,上述四个预定义角色并不是字符串,而是常量。因此必须以 'allow' => RBAC_EVERYONE 这样方式使用。并且不能和其他角色混用,例如 'allow' => RBAC_EVERYONE . ', POWER_USER' 就是错误的。

验证规则在验证时,首先从 session 中取出用户的角色信息。取出来的角色信息是一个数组,数组中每一个项为用户具有的一个角色。例如:
$userRoles = array( 'POWER_USER', 'MANAGER',);然后取出控制器的 ACT,再按照下列规则进行验证:
如果 ACT 的 allow 为 RBAC_EVERYONE,则进行下列检查:如果 ACT 的 deny 为 RBAC_NULL,则表示表示允许任何角色访问,验证结果为 true;如果 ACT 的 deny 为 RBAC_NO_ROLE,则表示用户只要具有角色信息,就可以访问。因此如果用户的角色信息为空白,则验证结果为 false,否则验证结果为 true;如果 ACT 的 deny 为 RBAC_HAS_ROLE,则表示用户只要具有角色信息,就不允许访问。因此如果用户的角色信息为空白,则验证结果为 true,否则验证结果为 false;如果 ACT 的 deny 为 RBAC_EVERYONE,则表示这个 ACT 存在冲突(因为 allow 和 deny 都为 RBAC_EVERYONE);检查用户的角色名是否出现在 deny 指定的角色名中,如果有,则验证结果为 false,否则验证结果为 true。如果 ACT 的 allow 为 RBAC_HAS_ROLE,则表示用户只要具有角色信息,就可以访问。因此如果用户的角色信息为空白,则验证结果为 false,否则验证结果为 true;如果 ACT 的 allow 为 RBAC_NO_ROLE,则表示用户只要具有角色信息,就不允许访问。因此如果用户的角色信息为空白,则验证结果为 true,否则验证结果为 false;如果 ACT 的 allow 为 RBAC_NULL,则进行下列检查:如果 ACT 的 deny 为 RBAC_NULL,则表示 ACT 既没有设置允许访问的角色,也没有设置拒绝访问的角色,这时候假定为允许访问,所以验证结果为 true;如果 ACT 的 deny 为 RBAC_NO_ROLE,则表示用户只要具有角色信息,就可以访问。因此如果用户的角色信息为空白,则验证结果为 false,否则验证结果为 true;如果 ACT 的 deny 为 RBAC_HAS_ROLE,则表示用户只要具有角色信息,就不允许访问。因此如果用户的角色信息为空白,则验证结果为 true,否则验证结果为 false;如果 ACT 的 deny 为 RBAC_EVERYONE,则表示拒绝任何角色访问,验证结果为 false;5) 检查用户用户的角色名是否出现在 deny 指定的角色名中,如果有,则验证结果为 false,否则验证结果为 true。验证进行到这里时,ACT 的 allow 必然是角色名,因此只要用户具有的角色名在 allow 指定的角色名中,验证结果就为true,否则验证结果为false。
ACT 示例之所有进行这么复杂的验证,是考虑到多种多样的验证需求。看看下面几个例子:
只要具有角色,就允许访问:array( 'allow' => RBAC_HAS_ROLE);只要具有角色,就不允许访问:array( 'deny' => RBAC_HAS_ROLE);用户具有角色,并且没有 POWER_USER 角色时,允许访问:array( 'allow' => RBAC_HAS_ROLE, 'deny' => 'POWER_USER')用户只要没有 MANAGER 角色,就允许访问:array( 'deny' => 'MANAGER',)用户具有 POWER_USER 和 MANAGER 角色时允许访问,但具有 SYSTEM_ADMIN 角色时拒绝访问。对于这个 ACT 定义,如果用户的角色为 &#8218OWER_USER, GUEST‘,则允许访问。如果用户的角色为 &#8218OWER_USER, SYSTEM_ADMIN‘,则不允许访问。因为 deny 的优先级总是大于 allow。array( 'allow' => 'POWER_USER, MANAGER', 'deny' => 'SYSTEM_ADMIN',)上面虽然只说了对控制器的验证,但对控制器动作的验证规则是完全相同的。只是只有当用户被允许访问控制器时,才会对要访问的控制器动作进行验证。这种机制提供非常高的灵活性,例如:
用户只要具有 POWER_USER 和 SYSTEM_ADMIN 两个角色之一,就可以访问这个控制器。但只有当用户具有 SYSTEM_ADMIN 角色时,才允许使用控制器的 remove 动作。反之,如果用户具有 SYSTEM_ADMIN 角色,就不允许使用控制器的 create 动作。
特别注意,不管是 allow 还是 deny,只要用户具有的角色有其中之一符合条件就会判定该规则有效。例如 'allow' => 'POWER_USER, SYSTEM_ADMIN' 只要用户具有 POWER_USER 和 SYSTEM_ADMIN 两个角色之一,就算作允许访问。而不需要用户同时具有 POWER_USER 和 SYSTEM_ADMIN 角色。

角色信息存储服务准备好控制器的 ACT 后,我们还需要使用角色信息存储服务,来管理应用程序中会用到的角色信息。
首先,我们要建立如下的数据表(假定使用 MySQL)。这个数据表很简单,每行记录存储一个角色。
CREATE TABLE `roles` ( `role_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY , `rolename` VARCHAR( 32 ) NOT NULL , `created` INT NULL , `updated` INT NULL);然后我们在应用程序中就可以从 FLEA_Com_RBAC_RolesManager 派生一个对象来管理角色信息了:
load_class('FLEA_Com_RBAC_RolesManager');class MyRolesManager extends FLEA_Com_RBAC_RolesManager{ var $tableName = 'roles'; var $primaryKey = 'role_id';}$rolesManager =& get_singleton('MyRolesManager');/* @var $rolesManager MyRolesManager */$role = array('rolename' => 'SYSTEM_ADMIN');$rolesManager->create($role);$role = array('rolename' => 'POWER_USER');$rolesManager->create($role);$role = array('rolename' => 'MANAGER');$rolesManager->create($role);事实上,FLEA_Com_RBAC_RolesManager 是一个表数据入口对象,所以可以直接使用 create()、find() 等方法来添加、查询角色信息。

用户信息存储服务只有角色信息,RBAC 还无法工作。我们还需要用户信息存储服务。这些服务由 FLEA_Com_RBAC_UsersManager 来提供。首先建立存储用户信息的数据表(仍然假定使用 MySQL):
CREATE TABLE `users` ( `user_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY , `username` VARCHAR( 32 ) NOT NULL , `password` VARCHAR( 64 ) NOT NULL , `email` VARCHAR( 128 ) NOT NULL , `created` INT NULL , `updated` INT NULL); 特别注意,存储用户信息的数据表中,至少需要用户名、密码和电子邮件地址三个字段。
然后我们就可以很方便的存储和查询用户信息了:
load_class('FLEA_Com_RBAC_UsersManager');class MyUsersManager extends FLEA_Com_RBAC_UsersManager{ var $tableName = 'users'; var $primaryKey = 'user_id';}$usersManager =& get_singleton('MyUsersManager');/* @var $usersManager MyUsersManager */$user = array( 'username' => 'dualface', 'password' => '12345678', 'email' => '[email protected]',);$usersManager->create($user);上面的代码会建立一个用户名为 dualface 的用户,而密码明文是 12345678。你也许会奇怪,难道密码不需要加密后存储吗?
实际上,FLEA_Com_RBAC_UsersManager 为你自动完成了该项工作。FLEA_Com_RBAC_UsersManager 在建立一个用户或者更新一个用户时,对于密码字段都是特别处理的。所以程序只需要提供密码明文就行了,而不需要自己加密。不过同时也要注意在更新用户时,不要更新密码字段。而是应该使用 FLEA_Com_RBAC_UsersManager::changePassword() 方法来修改用户账户的密码。
FLEA_Com_RBAC_UsersManager 采用什么加密方法来存储密码,是由 FLEA_Com_RBAC_UsersManager:encodeMethod 变量决定的。默认为 PWD_CRYPT,即使用 crypt() 函数来加密。可用的加密方式还有 PWD_MD5(存储用 md5() 函数编码后的密码)和 PWD_CLEARTEXT(存储密码明文)。由于不同的加密方式,生成的密码长度都不同,所以建立用户信息数据表时,密码字段的长度为 64,而不是常用的 32。
特别注意,crypt() 函数加密的密码超过了 32 个字符,因此一定要确保用户信息表的密码字段有足够的长度。
如果用户信息表的用户名、密码和电子邮件地址三个字段不是默认的 username、password 和 email,那么要分别通过 FLEA_Com_RBAC_UsersManager:usernameField、FLEA_Com_RBAC_UsersManager:passwordField 和 FLEA_Com_RBAC_UsersManager:emailField 变量来指定。
FLEA_Com_RBAC_UsersManager 虽然也是一个表数据入口,但是提供了 findByUserId()、findByUsername、findByEmail 等便利的方法。请参考 API 文档中的 FLEA_Com_RBAC_UsersManager 类,了解所有便利方法。

为用户指定角色有了用户和角色信息,我们还需要将用户信息和角色信息关联起来。这仍然是通过 FLEA_Com_RBAC_UsersManager 来完成。
首先也是建立一个数据表,用来存储用户和角色之间的关联关系:
CREATE TABLE `roles_users` ( `user_id` INT NOT NULL , `role_id` INT NOT NULL , PRIMARY KEY ( `user_id` , `role_id` ));接下来修改我们的 FLEA_Com_RBAC_UsersManager 继承类:
load_class('FLEA_Com_RBAC_UsersManager');class MyUsersManager extends FLEA_Com_RBAC_UsersManager{ var $tableName = 'users'; var $primaryKey = 'user_id'; var $rolesFields = 'roles'; var $manyToMany = array( 'tableClass' => 'MyRolesManager', 'mappingName' => 'roles', 'joinTable' => 'roles_users', );}新增加的一个 MANY_TO_MANY 用于将用户和角色关联起来。**注意 MANY_TO_MANY 关联的 mappingName 选项一定要和 $rolesFields 变量的值一样。否则用 FLEA_Com_RBAC_UsersManager::fetchRoles() 无法取得用户的角色信息。
不过只是这样的定义,还不能实际使用。我们看一段实际使用的代码:
上面的代码中第一次出现了对 FLEA_Com_RBAC 的使用。通常我们只需要用到 FLEA_Com_RBAC::setUser() 方法。这个方法将用户信息和对应的角色信息保存到 session。为了节约服务器资源,我们要尽量减少保存在 session 中的内容。
运行这个脚本,可以看到如下的输出:

大家在开发自己的应用程序时,基本上可以把 login() 函数的内容照搬过去。

用户注销处理用户注销非常简单,通常用 session_destroy() 销毁 session 数据就可以了。如果只想清除用户登录信息,而不影响 session 中的其他信息,可以用下面两行代码:
$rbac =& get_singleton('FLEA_Com_RBAC');$rbac->clearUser();
实现访问控制实际上,做完上面几个步骤,我们的 RBAC 已经可以工作了。你可以尝试登录系统,然后访问那些受到保护的控制器。然后再从系统注销后,重新访问受保护的控制器。
目前,FleaPHP 的 RBAC 在处理 ACT 上,还不够灵活。每个控制器的 ACT 都是从文件载入的,而不是从数据库。但有聪明的开发者已经想出了变通的做法。
那就是把从数据库获取控制器 ACT 的代码写在控制器的 .act.php 文件中,例如:
APP\Controller\MyController.act.php
当然,我们还要实现一个 Model_ControllerACTProvider 表数据入口:
以及一个数据表:
CREATE TABLE `controller_acts` ( `controller_name` VARCHAR( 32 ) NOT NULL , `act` TEXT NOT NULL , `created` INT NULL , `updated` INT NULL , PRIMARY KEY ( `controller_name` ))这样一来,我们就可以在数据库中保存控制器的 ACT 了。

作者: jake   发布时间: 2009-04-20

以前对这个,不知道了解,现在深入学习

作者: 太阳神   发布时间: 2009-04-20

好帖。顶顶顶

作者: wokao   发布时间: 2009-06-17

内容不错哦

作者: ahsxw255   发布时间: 2009-06-19

相关阅读 更多