返回目录
目录
返回目录
使用 Zend Framework 建造多用户应用程序
当原始网络被创造的时候,它被设计成一个发布以静态内容为主的平台。随着对网络内容需求的增长,网络内容的互联网用户数量的增长,把网络当作一个应用平台的需求也随之增长。由于网络天生擅长从一个单独的地点同时传递经验给许多用户,这使得它成为这样一个理想环境:建设动态驱动,多用户的,以及现在更常见的,社交系统。
HTTP 是 web 网络的协议:一个无状态的,典型短暂存在的,请求和反应的协议。之所以这样设计这个协议是因为,原始的互联网是用来服务或者发布静态内容。恰恰是这样设计,使得 web 正如它本身一样取得巨大的成功。也正是这个设计给想把 web 当作一个应用平台使用的开发者带来了新的关注。
这些关注和职责,可以被以下三个问题有效的总结:
注意:消费者 对 用户
注意我们使用消费者这个词组,而不是个人。越来越多的,web 应用程序变得服务驱动。这意味着不止真实的人(用户)使用真实的 web 浏览器在消费和使用你的应用程序,同时其它的 web 应用程序通过机器服务技术,诸如 REST,SOAP,和 XML-RPC,在做同样的事。在这方面,基于上述的考虑,人,如同其它的消费应用程序,应该被一视同仁。
在下面的章节中,我们将仔细研究这些与认证和授权相关的通常问题。我们将包括三个主要的组件:Zend_Session,Zend_Auth,和 Zend_Acl 如何提供一个 盒子之外(out-of-the-box)的解决办法以及它们每一个所拥有的迎合一个更加定制化的解决办法的扩展关键点。
返回目录
在 ZF 管理用户对话
web 的成功深深的根源于驱动 web 的协议:HTTP。HTTP 胜过 TCP 是由于它天生的无状态,这意味着 web 的内部也是无状态的。这是 web 成为如此普及的媒体的主要原因之一,同时它也给想把 web 当作一个应用程序平台来使用的开发者带来一个有意思的问题。
和一个 web 应用互动的动作通常取决于发送给一个 web 服务器的全部请求的总数。由于可能同时服务于许多的消费者,应用程序必须决定请求属于哪个消费者。这些请求通常被认作是对话。
在 PHP,对话问题通过对话扩展解决,这些扩展使用一些状态跟踪,通常是 cookie,和一些本地存储的形式(可以通过 $_SESSION 全局变量访问)。在 Zend Framework,Zend_Session 组件把值添加到 PHP 对话扩展中,使得它更易于使用和依赖于内置的面向对象的应用程序。
Zend_Session 组件是一个对话管理器,同时也是一个把数据存储进一个长时间存在的对话对象的 API。Zend_Session API 用来管理一个对话的选项和行为,例如选择,开始和停止一个对话,然而 Zend_Session_Namespace 才是真正用来存储数据的对象。
虽然在一个 bootstrap 进程内开始一个对话通常是好的实践,但是这通常不是必须的,因为所有的对话将会在一个 Zend_Session_Namespace 对象第一次创建的时候自动开始。
作为 Zend_Application_Resource 系统的一部分,Zend_Application 有能力为你配置 Zend_Session。为了使用这个,假设你的项目使用 Zend_Applicaiont 来启动(bootstrap),你可以把下面的代码添加到你的 applicaiont.ini 文件中:
resources.session.save_path = APPLICATION_PATH "/../data/session" resources.session.use_only_cookies = true resources.session.remember_me_seconds = 864000
正如你可以看到的,传递的选项和你期望在 PHP ext/session 扩展中发现的选项是一致的。这些选项设置了项目内,数据将要被储存的对话文件的路径。由于 ini 文件可以额外的使用常量,上面的代码将使用 APPLICATION_PATH 常量和到一个对话数据目录的相对路径。
大部分使用对话的 Zend Framework 组件不再需要其它任何东西,就可以使用 Zend_Session 了。到现在为止,你既可以使用一个组件来消费 Zend_Session,或者使用 Zend_Session_Namespace 来开始把你自己的数据存储到一个对话内。
Zend_Session_Namespace 是一个简单的类,它通过一个易于使用的 API,代理数据进入由 Zend_Session 管理的 $_SESSION 超全局变量。它之所以被称为 Zend_Session_Namespace 是因为它有效的命名 $_SESSION 内部的数据,从而允许数量众多的组件和对象安全的存储和检索数据。在下面的代码中,我们会展示如何建造一个简单的对话递增计数器,从1000开始,1999以后重置自己。
$mysession = Zend_Session_Namespace('mysession'); if (!isset($mysession->counter)) { $mysession->counter = 1000; } else { $mysession->counter++; } if ($mysession->counter > 1999) { unset($mysession->counter); }
你从上面可以看见,对话命名空间对象使用魔术 __get,__set,__isset,和 __unset 来使你和对话无缝和流畅的互动。上面例子中存储的信息被储存在 $_SESSION['mysession']['counter']。
另外,如果你想对 Zend_Session 使用 DbTable 保存句柄,你得把下面的代码添加到你的 application.ini:
resources.session.saveHandler.class = "Zend_Session_SaveHandler_DbTable" resources.session.saveHandler.options.name = "session" resources.session.saveHandler.options.primary.session_id = "session_id" resources.session.saveHandler.options.primary.save_path = "save_path" resources.session.saveHandler.options.primary.name = "name" resources.session.saveHandler.options.primaryAssignment.sessionId = "sessionId" resources.session.saveHandler.options.primaryAssignment.sessionSavePath = "sessionSavePath" resources.session.saveHandler.options.primaryAssignment.sessionName = "sessionName" resources.session.saveHandler.options.modifiedColumn = "modified" resources.session.saveHandler.options.dataColumn = "session_data" resources.session.saveHandler.options.lifetimeColumn = "lifetime"
返回目录
在 Zend Framework 中鉴别用户
一旦一个 web 应用程序通过建立一个对话把一个用户和其它的用户区分开来,web 应用程序典型的想确认一个用户的身份。确认一个消费者是一个真正的消费者,这个过程就是鉴别(authentication)。鉴别由两个特别的部分组成:一个身份证和一组证书。为了能够鉴别一个用户,出现在应用程序中的这两个部分需要一些变化。
当大部分通常的鉴别模式以用户名和密码为中心的时候,应该指出的是,这不是全部的事实。身份证不仅局限于用户名。事实上,可以使用任何的公共身分证:一个分配的号码,社会安全号,或者居住地址。同样的,证书不仅仅局限于密码。证书可以受保护的私人信息的形式出现:指纹,眼睛视网膜扫描,密语(通行语),或者其它任何鲜为人知的个人信息。
在下面的例子中,我们将会使用 Zend_Auth 来完成可能是最丰富的鉴别形式:在一个数据库表格中的用户名和密码。这个例子假设你已经使用 Zend_Application 建立了你的应用程序,而且在应用程序内部你已经配置好了一个数据库链接。
Zend_Auth 类的工作由两部分组成。首先,它应该可以接受一个鉴别适配器用来鉴别一个用户。其次,对一个用户成功认证以后,它应该持续遍及各个和每一个可能想知道现在的用户是否真正被认证的请求。为了保持数据,Zend_Auth 消费 Zend_Session_Namespace,但是通常的,你将不需要和这个对话对象互动。
让我们假设我们已经有了如下的数据库表格:
CREATE TABLE users ( id INTEGER NOT NULL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(32) NULL, password_salt VARCHAR(32) NULL, real_name VARCHAR(150) NULL )
以上的代码展示了一个包括一个用户名,密码,还有一个密码盐字段的用户表格。这个盐字段是用作一个被称为 salting 技术的一部分,这个技术会提高你的数据库安全,以你的密码哈希算法为目标的暴力攻击。更多关于 salting 的信息。
对于这个应用,我们必须首先创建一个简单的表单,可以用作登陆表单(login form)。我们将使用 Zend_Form 来完成:
// located at application/forms/Auth/Login.php class Default_Form_Auth_Login extends Zend_Form { public function init() { $this->setMethod('post'); $this->addElement( 'text', 'username', array( 'label' => 'Username:', 'required' => true, 'filters' => array('StringTrim'), )); $this->addElement('password', 'password', array( 'label' => 'Password:', 'required' => true, )); $this->addElement('submit', 'submit', array( 'ignore' => true, 'label' => 'Login', )); } }
使用以上的表单,我们现在可以开始为我们的鉴别控制器创建我们的登陆行为。这个控制器将被命名为 AuthController,而且将会被放置在 application/controllers/AuthController.php。它会有一个单一的方法 loginAction() 被当作自己传递(self-posting)行为使用。也就是说,不管 url 是被 post 还是 get,这个方法将可以处理这个逻辑。
下面的代码将展示如何建造合适的适配器,与表单集成:
class AuthController extends Zend_Controller_Action { public function loginAction() { $db = $this->_getParam('db'); $loginForm = new Default_Form_Auth_Login($_POST); if ($loginForm->isValid()) { $adapter = new Zend_Auth_Adapter_DbTable( $db, 'users', 'password', 'MD5(CONCAT(?, password_salt))' ); $adapter->setIdentity($loginForm->getValue('username')); $adapter->setCredential($loginForm->getValue('password')); $result = $auth->authenticate($adapter); if ($result->isValid()) { $this->_helper->FlashMessenger('Successful Login'); $this->redirect('/'); return; } } $this->view->loginForm = $loginForm; } }
与这个行为相对应的视图脚本十分简单。因为这个表单是自己提交,所以它会设置当前的 url,然后它会显示表单。这个视图脚本放置在 application/views/scripts/auth/login.phtml:
$this->form->setAction($this->url()); echo $this->form;
你拥有它了。利用这些基础知识,你可以扩展普通的概念,来包括更复杂的鉴别情景。浏览指导手册获得更多信息。
返回目录
在 Zend Framework 内建造一个授权系统
在一个用户被认证为合法用户以后,一个应用程序就忙于给一个消费者提供一些有用和期望的资源。在许多例子中,应用程序可能包括不同的资源种类,同时某些资源还有严格的访问规则。这个确定谁有权访问哪一个资源的过程就是授权。最简单的授权形式由这些元素组成:
在 Zend Framework,Zend_Acl 组件处理这些任务:建造一个角色,资源,权限的树状图,来管理和询问授权请求。
当使用 Zend_Acl,只需简单的应用合适的接口,任何的模型(model)可以充当角色或是资源。为了以一个角色功能使用,类必须应用 Zend_Acl_Role_Interface,它只要求有 getRoleId()。为了以一个资源功能使用,一个类必须应用 Zend_Acl_Resource_Interface,类似的,它要示类应用 getResourceId() 这个方法。
下面展示的是一个简单的用户模型。这个模型只是简单的应用了 Zend_Acl_Role_Interface 就能参与到我们的 ACL 系统。当一个 ID 未知的时候,getRoleId() 方法会返回 guest 这个ID,否则它会返回分配给这个事实用户对象的角色 ID。这个值会有效来自任何地方,一个静态定义或者可能动态的来自用户数据库角色本身。
class Default_Model_User implements Zend_Acl_Role_Interface { protected $_aclRoleId = null; public function getRoleId() { if ($this->_aclRoleId == null) { return 'guest'; } return $this->_aclRoleId; } }
现在我们有了至少一个角色和一个资源,我们可以着手定义 ACL 系统的规则。当系统接收到一个关于什么将会给予一个特定角色,资源和可选择的一个权限的时候,这些规则将会应用。
让我们假设以下的规则:
$acl = new Zend_Acl(); // setup the various roles in our system $acl->addRole('guest'); // owner inherits all of the rules of guest $acl->addRole('owner', 'guest'); // add the resources $acl->addResource('blogPost'); // add privileges to roles and resource combinations $acl->allow('guest', 'blogPost', 'view'); $acl->allow('owner', 'blogPost', 'post'); $acl->allow('owner', 'blogPost', 'publish');
以上的规则是十分简单的:一个 guest 角色和一个 owner 角色存在;同时还有一个 blogPost 类型的资源。guest 用户被允许浏览博客帖子,owner 被允许发表和发布帖子。为了向系统查询,可以按照以下的任何:
// assume the user model is of type guest resource $guestUser = new Default_Model_User(); $ownerUser = new Default_Model_Owner('OwnersUsername'); $post = new Default_Model_BlogPost(); $acl->isAllowed($guestUser, $post, 'view'); // true $acl->isAllowed($ownerUser, $post, 'view'); // true $acl->isAllowed($guestUser, $post, 'post'); // false $acl->isAllowed($ownerUser, $post, 'post'); // true
正如你看见的,以上的规则执行了 owners 和 guest 是否能浏览帖子,可以浏览哪个帖子,或者发表新的帖子,哪一个 owner 可以而 guest 不能。但是你可能希望这种系统可能不如我们希望它成为的那么动态。如果我们想确保一个特殊的 owner 在允许他发表帖子以前,确实是拥有一个十分特殊的帖子的能力?也就是说,我们想确保帖子的所有者才有发表他们自己帖子的能力。
这里就是断言(assertion)出现的地方了。断言是方法,当静态的规则检查不足够的时候,它们会被调用。当注册一个断言对象,这个对象会帮助确认,典型的是动态的,是否一些角色可以访问一些资源,带有一些可选择的,只能由断言内部的逻辑回答的权限。对于这个例子,我们使用以下的断言:
class OwnerCanPublishBlogPostAssertion implements Zend_Acl_Assert_Interface { /** * This assertion should receive the actual User and BlogPost objects. * * @param Zend_Acl $acl * @param Zend_Acl_Role_Interface $user * @param Zend_Acl_Resource_Interface $blogPost * @param $privilege * @return bool */ public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $user = null, Zend_Acl_Resource_Interface $blogPost = null, $privilege = null) { if (!$user instanceof Default_Model_User) { throw new Exception(__CLASS__ . '::' . __METHOD__ . ' expects the role to be' . ' an instance of User'); } if (!$blogPost instanceof Default_Model_BlogPost) { throw new Exception(__CLASS__ . '::' . __METHOD__ . ' expects the resource to be' . ' an instance of BlogPost'); } // If role is publisher, he can always modify a post if ($user->getRoleId() == 'publisher') { return true; } // Check to ensure that everyon else is only modifying their own post if ($user->id != null && $blogPost->ownerUserId == $user->id) { return true; } else { return false; } } }
把这个挂到我们的 ACL 系统,我们会做以下事情:
// replace this: // $acl->allow('owner', 'blogPost', 'publish'); // with this: $acl->allow('owner', 'blogPost', 'publish', new OwnerCanPublishBlogPostAssertion()); // let's also add the role of a "publisher" who has access to everything $acl->allow('publisher', 'blogPost', 'publish');
现在,任何时候,ACL 被咨询关于一个 owner 是否能还是不能发表一个特殊帖子的时候,断言将被运行。这个断言确保,除非角色类型是 publisher,owner 角色必须逻辑上与被询问的博客帖子有关。在这个例子中,我们检查了博客帖子的 ownerUserId 属性和传递进来的 owner id 相匹配。