教程 3: 保护INVO(Tutorial 3: Securing INVO)

在这一章, 我们将继续解释INVO是如何构成的, 我们将讨论认证的实施, 使用事件和插件的认证和一个由Phalcon管理的访问控制列表.

登录应用(Log into the Application)

一个 “log in” 功能将允许我们在后台控制器中工作. 后台控制器和前台之前的分离是合乎逻辑的. 所有加载的控制器都位于相同的目录 (app/controllers/).

为了进入系统, 用户必须有一个有效的用户名和密码. 用户存储在数据库 “invo” 里面的 “users” 表里面.

在我们开始会话之前, 我们需要在数据库配置数据库的连接. 一个 “db” 服务在服务容器中设置连接信息. 就自动加载器来说, 我们再一次从配置文件中读取参数来配置一个服务:

  1. <?php
  2. use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter;
  3. // ...
  4. // 数据库连接是基于配置文件已经定义的参数创建的
  5. $di->set(
  6. "db",
  7. function () use ($config) {
  8. return new DbAdapter(
  9. [
  10. "host" => $config->database->host,
  11. "username" => $config->database->username,
  12. "password" => $config->database->password,
  13. "dbname" => $config->database->name,
  14. ]
  15. );
  16. }
  17. );

这里, 我们将会返回一个MySQL连接适配器的一个实例. 如果需要, 你可以做一些额外的操作比如添加一个日志记录, 一个分析器或者改变适配器, 设置你想要的.

下列表单(app/views/session/index.volt) 请求登录信息. 我们已经删除了一些 HTML 代码来让例子更加简洁:

  1. {{ form("session/start") }}
  2. <fieldset>
  3. <div>
  4. <label for="email">
  5. Username/Email
  6. </label>
  7. <div>
  8. {{ text_field("email") }}
  9. </div>
  10. </div>
  11. <div>
  12. <label for="password">
  13. Password
  14. </label>
  15. <div>
  16. {{ password_field("password") }}
  17. </div>
  18. </div>
  19. <div>
  20. {{ submit_button("Login") }}
  21. </div>
  22. </fieldset>
  23. {{ endForm() }}

使用原生的PHP作为以前的教程, 我们开始使用 Volt. 这是一个内置的模板引擎受 Jinja 的影响而提供简单而又友好的语法来创建模板. 在你熟悉 Volt 之前, 它将不会花费你太多的时间.

SessionController::startAction 方法 (app/controllers/SessionController.php) 有验证表单中输入的数据包括检查在数据库中是否为有效用户的任务:

  1. <?php
  2. class SessionController extends ControllerBase
  3. {
  4. // ...
  5. private function _registerSession($user)
  6. {
  7. $this->session->set(
  8. "auth",
  9. [
  10. "id" => $user->id,
  11. "name" => $user->name,
  12. ]
  13. );
  14. }
  15. /**
  16. * 这个方法检验和记录一个用户到应用中
  17. */
  18. public function startAction()
  19. {
  20. if ($this->request->isPost()) {
  21. // 从用户获取数据
  22. $email = $this->request->getPost("email");
  23. $password = $this->request->getPost("password");
  24. // 在数据库中查找用户
  25. $user = Users::findFirst(
  26. [
  27. "(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'",
  28. "bind" => [
  29. "email" => $email,
  30. "password" => sha1($password),
  31. ]
  32. ]
  33. );
  34. if ($user !== false) {
  35. $this->_registerSession($user);
  36. $this->flash->success(
  37. "Welcome " . $user->name
  38. );
  39. // 如果用户是有效的, 转发到'invoices'控制器
  40. return $this->dispatcher->forward(
  41. [
  42. "controller" => "invoices",
  43. "action" => "index",
  44. ]
  45. );
  46. }
  47. $this->flash->error(
  48. "Wrong email/password"
  49. );
  50. }
  51. // 再一次转发到登录表单
  52. return $this->dispatcher->forward(
  53. [
  54. "controller" => "session",
  55. "action" => "index",
  56. ]
  57. );
  58. }
  59. }

为简单起见, 我们使用 “sha1” 在数据库中存储密码散列, 然而, 在实际应用中不建议采用此算法, 使用 “bcrypt” 代替.

请注意, 多个公共属性在控制器访问, 像: $this->flash, $this->request 或者 $this->session. 这些是先前在服务容器中定义的服务 (app/config/services.php). 当它们第一次访问的时候, 它们被注入作为控制器的一部分.

这些服务是”共享”的, 这意味着我们总是访问相同的地方, 无论我们在哪里调用它们.

例如, 这里我们调用 “session” 服务然后我们在变量 “auth” 中存储用户身份:

  1. <?php
  2. $this->session->set(
  3. "auth",
  4. [
  5. "id" => $user->id,
  6. "name" => $user->name,
  7. ]
  8. );

本节的另外一个重要方面是如何验证用户为有效的, 首先我们验证是否使用的是POST请求的:

  1. <?php
  2. if ($this->request->isPost()) {

然后, 我们接收表单中的参数:

  1. <?php
  2. $email = $this->request->getPost("email");
  3. $password = $this->request->getPost("password");

现在, 我们需要检查是否存在一个相同的用户名或邮箱和密码的用户:

  1. <?php
  2. $user = Users::findFirst(
  3. [
  4. "(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'",
  5. "bind" => [
  6. "email" => $email,
  7. "password" => sha1($password),
  8. ]
  9. ]
  10. );

注意, ‘绑定参数’的使用, 占位符 :email: 和 :password: 要放置在替换的值的位置, 然后值的’绑定’使用参数 ‘bind’. 安全的替换列的值而没有SQL注入的危险.

如果用户是有效的, 我们将会在session中注册它, 并且转发到dashboard:

  1. <?php
  2. if ($user !== false) {
  3. $this->_registerSession($user);
  4. $this->flash->success(
  5. "Welcome " . $user->name
  6. );
  7. return $this->dispatcher->forward(
  8. [
  9. "controller" => "invoices",
  10. "action" => "index",
  11. ]
  12. );
  13. }

如果用户不存在,再一次转发到登录表单让用户再次操作:

  1. <?php
  2. return $this->dispatcher->forward(
  3. [
  4. "controller" => "session",
  5. "action" => "index",
  6. ]
  7. );

后端安全(Securing the Backend)

后端是一个私有区域,只有已经注册的人可以访问. 因此, 只有注册用户才能访问控制器这样的检验是有必要的. 如果你没有登录到应用中并试图访问, 例如, products 控制器 (这是私有的) 你将会看到如下屏幕:

../_images/invo-2.png

每次有人试图访问任何controller/action, 应用将会验证当前角色(在session中)是否能够访问它, 否则就会显示一个像上面那样的消息并转发到首页.

现在, 让我们看看应用程序是如何实现的. 首先我们知道有个组件叫做 Dispatcher. 通过 Routing 组件来找到路由. 然后, 它负责加载合适的控制器和执行相应的动作方法.

正常情况下, 框架会自动创建分发器. 对我们而言, 我们想在执行请求的方法之前执行一个验证, 校验用户是否可以访问它. 要做到这一点, 我们需要在启动文件中创建一个方法来替换组件:

  1. <?php
  2. use Phalcon\Mvc\Dispatcher;
  3. // ...
  4. /**
  5. * MVC 分发器
  6. */
  7. $di->set(
  8. "dispatcher",
  9. function () {
  10. // ...
  11. $dispatcher = new Dispatcher();
  12. return $dispatcher;
  13. }
  14. );

我们现在使用完全控制的分发器用于应用程序. 在框架中需要多组件的触发事件, 允许我们能够修改内部的操作流. 依赖注入组件作为胶水的一部分, 一个新的叫做 EventsManager 的组件允许我们拦截由组件产生的事件, 路由事件到监听.

事件管理(Events Management)

一个 EventsManager 允许我们为一个特定类型的事件添加监听. 现在我们感兴趣的类型是 “dispatch”. 下列代码过滤了由分发器产生的所有事件:

  1. <?php
  2. use Phalcon\Mvc\Dispatcher;
  3. use Phalcon\Events\Manager as EventsManager;
  4. $di->set(
  5. "dispatcher",
  6. function () {
  7. // 创建一个事件管理器
  8. $eventsManager = new EventsManager();
  9. // 监听分发器中使用安全插件产生的事件
  10. $eventsManager->attach(
  11. "dispatch:beforeExecuteRoute",
  12. new SecurityPlugin()
  13. );
  14. // 处理异常和使用 NotFoundPlugin 未找到异常
  15. $eventsManager->attach(
  16. "dispatch:beforeException",
  17. new NotFoundPlugin()
  18. );
  19. $dispatcher = new Dispatcher();
  20. // 分配事件管理器到分发器
  21. $dispatcher->setEventsManager($eventsManager);
  22. return $dispatcher;
  23. }
  24. );

当一个叫做 “beforeExecuteRoute” 的事件触发以下插件将会被通知:

  1. <?php
  2. /**
  3. * 检验用户是否允许使用 SecurityPlugin 访问某些方法
  4. */
  5. $eventsManager->attach(
  6. "dispatch:beforeExecuteRoute",
  7. new SecurityPlugin()
  8. );

当一个 “beforeException” 被触发然后其他插件通知:

  1. <?php
  2. /**
  3. * 处理异常和使用 NotFoundPlugin 未找到异常
  4. */
  5. $eventsManager->attach(
  6. "dispatch:beforeException",
  7. new NotFoundPlugin()
  8. );

SecurityPlugin 是一个类位于(app/plugins/SecurityPlugin.php). 这个类实现了 “beforeExecuteRoute” 方法. 这是一个相同的名字在分发器中产生的事件中的一个:

  1. <?php
  2. use Phalcon\Events\Event;
  3. use Phalcon\Mvc\User\Plugin;
  4. use Phalcon\Mvc\Dispatcher;
  5. class SecurityPlugin extends Plugin
  6. {
  7. // ...
  8. public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
  9. {
  10. // ...
  11. }
  12. }

钩子事件始终接收第一个包含上下文信息所产生的事件($event)的参数和第二个包含事件本身所产生的对象($dispatcher)的参数. 这不是一个强制性的插件扩展类 Phalcon\Mvc\User\Plugin, 但通过这样做, 它们更容易获得应用程序中可用的服务.

现在, 我们验证当前 session 中的角色, 验证用户是否可以通过ACL列表访问.如果用户没有权限, 我们将会重定向到如上所述的主页中去:

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Events\Event;
  4. use Phalcon\Mvc\User\Plugin;
  5. use Phalcon\Mvc\Dispatcher;
  6. class SecurityPlugin extends Plugin
  7. {
  8. // ...
  9. public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
  10. {
  11. // 检查session中是否存在"auth"变量来定义当前活动的角色
  12. $auth = $this->session->get("auth");
  13. if (!$auth) {
  14. $role = "Guests";
  15. } else {
  16. $role = "Users";
  17. }
  18. // 从分发器获取活动的 controller/action
  19. $controller = $dispatcher->getControllerName();
  20. $action = $dispatcher->getActionName();
  21. // 获得ACL列表
  22. $acl = $this->getAcl();
  23. // 检验角色是否允许访问控制器 (resource)
  24. $allowed = $acl->isAllowed($role, $controller, $action);
  25. if (!$allowed) {
  26. // 如果没有访问权限则转发到 index 控制器
  27. $this->flash->error(
  28. "You don't have access to this module"
  29. );
  30. $dispatcher->forward(
  31. [
  32. "controller" => "index",
  33. "action" => "index",
  34. ]
  35. );
  36. // 返回 "false" 我们将告诉分发器停止当前操作
  37. return false;
  38. }
  39. }
  40. }

提供 ACL 列表(Providing an ACL list)

在上面的例子中我们已经获得了ACL的使用方法 $this->getAcl(). 这个方法也是在插件中实现的. 现在我们要逐步解释如何建立访问控制列表(ACL):

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Role;
  4. use Phalcon\Acl\Adapter\Memory as AclList;
  5. // 创建一个 ACL
  6. $acl = new AclList();
  7. // 默认行为是 DENY(拒绝) 访问
  8. $acl->setDefaultAction(
  9. Acl::DENY
  10. );
  11. // 注册两个角色, 用户是已注册用户和没有定义身份的来宾用户
  12. $roles = [
  13. "users" => new Role("Users"),
  14. "guests" => new Role("Guests"),
  15. ];
  16. foreach ($roles as $role) {
  17. $acl->addRole($role);
  18. }

现在, 我们分别为每个区域定义资源. 控制器名称是资源它们的方法是对资源的访问:

  1. <?php
  2. use Phalcon\Acl\Resource;
  3. // ...
  4. // 私有区域资源 (后台)
  5. $privateResources = [
  6. "companies" => ["index", "search", "new", "edit", "save", "create", "delete"],
  7. "products" => ["index", "search", "new", "edit", "save", "create", "delete"],
  8. "producttypes" => ["index", "search", "new", "edit", "save", "create", "delete"],
  9. "invoices" => ["index", "profile"],
  10. ];
  11. foreach ($privateResources as $resourceName => $actions) {
  12. $acl->addResource(
  13. new Resource($resourceName),
  14. $actions
  15. );
  16. }
  17. // 公共区域资源 (前台)
  18. $publicResources = [
  19. "index" => ["index"],
  20. "about" => ["index"],
  21. "register" => ["index"],
  22. "errors" => ["show404", "show500"],
  23. "session" => ["index", "register", "start", "end"],
  24. "contact" => ["index", "send"],
  25. ];
  26. foreach ($publicResources as $resourceName => $actions) {
  27. $acl->addResource(
  28. new Resource($resourceName),
  29. $actions
  30. );
  31. }

ACL现在了解现有的控制器和它们相关的操作. 角色 “Users” 由权限访问前台和后台的所有资源. 角色 “Guests” 仅允许访问公共区域:

  1. <?php
  2. // 授权user和Grant访问公共区域
  3. foreach ($roles as $role) {
  4. foreach ($publicResources as $resource => $actions) {
  5. $acl->allow(
  6. $role->getName(),
  7. $resource,
  8. "*"
  9. );
  10. }
  11. }
  12. // 授权仅角色Users 访问私有区域
  13. foreach ($privateResources as $resource => $actions) {
  14. foreach ($actions as $action) {
  15. $acl->allow(
  16. "Users",
  17. $resource,
  18. $action
  19. );
  20. }
  21. }

万岁!, ACL现在终于完成了. 在下一章, 我们将会看到Phalcon中的CRUD是如何实现的并且你如何自定义它.