# 教程 3: 保護INVO(Tutorial 3: Securing INVO)
在這一章, 我們將繼續解釋INVO是如何構成的, 我們將討論認證的實施, 使用事件和插件的認證和一個由Phalcon管理的訪問控制列表.
## 登錄應用(Log into the Application)
一個 “log in” 功能將允許我們在后臺控制器中工作. 后臺控制器和前臺之前的分離是合乎邏輯的. 所有加載的控制器都位于相同的目錄 (app/controllers/).
為了進入系統, 用戶必須有一個有效的用戶名和密碼. 用戶存儲在數據庫 “invo” 里面的 “users” 表里面.
在我們開始會話之前, 我們需要在數據庫配置數據庫的連接. 一個 “db” 服務在服務容器中設置連接信息. 就自動加載器來說, 我們再一次從配置文件中讀取參數來配置一個服務:
~~~
<?php
use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter;
// ...
// 數據庫連接是基于配置文件已經定義的參數創建的
$di->set(
"db",
function () use ($config) {
return new DbAdapter(
[
"host" => $config->database->host,
"username" => $config->database->username,
"password" => $config->database->password,
"dbname" => $config->database->name,
]
);
}
);
~~~
這里, 我們將會返回一個MySQL連接適配器的一個實例. 如果需要, 你可以做一些額外的操作比如添加一個日志記錄, 一個分析器或者改變適配器, 設置你想要的.
下列表單(app/views/session/index.volt) 請求登錄信息. 我們已經刪除了一些 HTML 代碼來讓例子更加簡潔:
~~~
{{ form("session/start") }}
<fieldset>
<div>
<label for="email">
Username/Email
</label>
<div>
{{ text_field("email") }}
</div>
</div>
<div>
<label for="password">
Password
</label>
<div>
{{ password_field("password") }}
</div>
</div>
<div>
{{ submit_button("Login") }}
</div>
</fieldset>
{{ endForm() }}
~~~
使用原生的PHP作為以前的教程, 我們開始使用[Volt](http://docs.iphalcon.cn/reference/volt.html). 這是一個內置的模板引擎受[Jinja](http://jinja.pocoo.org/)的影響而提供簡單而又友好的語法來創建模板. 在你熟悉 Volt 之前, 它將不會花費你太多的時間.
`SessionController::startAction`方法 (app/controllers/SessionController.php) 有驗證表單中輸入的數據包括檢查在數據庫中是否為有效用戶的任務:
~~~
<?php
class SessionController extends ControllerBase
{
// ...
private function _registerSession($user)
{
$this->session->set(
"auth",
[
"id" => $user->id,
"name" => $user->name,
]
);
}
/**
* 這個方法檢驗和記錄一個用戶到應用中
*/
public function startAction()
{
if ($this->request->isPost()) {
// 從用戶獲取數據
$email = $this->request->getPost("email");
$password = $this->request->getPost("password");
// 在數據庫中查找用戶
$user = Users::findFirst(
[
"(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'",
"bind" => [
"email" => $email,
"password" => sha1($password),
]
]
);
if ($user !== false) {
$this->_registerSession($user);
$this->flash->success(
"Welcome " . $user->name
);
// 如果用戶是有效的, 轉發到'invoices'控制器
return $this->dispatcher->forward(
[
"controller" => "invoices",
"action" => "index",
]
);
}
$this->flash->error(
"Wrong email/password"
);
}
// 再一次轉發到登錄表單
return $this->dispatcher->forward(
[
"controller" => "session",
"action" => "index",
]
);
}
}
~~~
為簡單起見, 我們使用 “[sha1](http://php.net/manual/zh/function.sha1.php)” 在數據庫中存儲密碼散列, 然而, 在實際應用中不建議采用此算法, 使用 “[bcrypt](http://docs.iphalcon.cn/reference/security.html)” 代替.
請注意, 多個公共屬性在控制器訪問, 像:`$this->flash`,`$this->request`或者`$this->session`. 這些是先前在服務容器中定義的服務 (app/config/services.php). 當它們第一次訪問的時候, 它們被注入作為控制器的一部分.
這些服務是”共享”的, 這意味著我們總是訪問相同的地方, 無論我們在哪里調用它們.
例如, 這里我們調用 “session” 服務然后我們在變量 “auth” 中存儲用戶身份:
~~~
<?php
$this->session->set(
"auth",
[
"id" => $user->id,
"name" => $user->name,
]
);
~~~
本節的另外一個重要方面是如何驗證用戶為有效的, 首先我們驗證是否使用的是POST請求的:
~~~
<?php
if ($this->request->isPost()) {
~~~
然后, 我們接收表單中的參數:
~~~
<?php
$email = $this->request->getPost("email");
$password = $this->request->getPost("password");
~~~
現在, 我們需要檢查是否存在一個相同的用戶名或郵箱和密碼的用戶:
~~~
<?php
$user = Users::findFirst(
[
"(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'",
"bind" => [
"email" => $email,
"password" => sha1($password),
]
]
);
~~~
注意, ‘綁定參數’的使用, 占位符 :email: 和 :password: 要放置在替換的值的位置, 然后值的’綁定’使用參數 ‘bind’. 安全的替換列的值而沒有SQL注入的危險.
如果用戶是有效的, 我們將會在session中注冊它, 并且轉發到dashboard:
~~~
<?php
if ($user !== false) {
$this->_registerSession($user);
$this->flash->success(
"Welcome " . $user->name
);
return $this->dispatcher->forward(
[
"controller" => "invoices",
"action" => "index",
]
);
}
~~~
如果用戶不存在,再一次轉發到登錄表單讓用戶再次操作:
~~~
<?php
return $this->dispatcher->forward(
[
"controller" => "session",
"action" => "index",
]
);
~~~
## 后端安全(Securing the Backend)
后端是一個私有區域,只有已經注冊的人可以訪問. 因此, 只有注冊用戶才能訪問控制器這樣的檢驗是有必要的. 如果你沒有登錄到應用中并試圖訪問, 例如, products 控制器 (這是私有的) 你將會看到如下屏幕:

每次有人試圖訪問任何controller/action, 應用將會驗證當前角色(在session中)是否能夠訪問它, 否則就會顯示一個像上面那樣的消息并轉發到首頁.
現在, 讓我們看看應用程序是如何實現的. 首先我們知道有個組件叫做[Dispatcher](http://docs.iphalcon.cn/reference/dispatching.html). 通過[Routing](http://docs.iphalcon.cn/reference/routing.html)組件來找到路由. 然后, 它負責加載合適的控制器和執行相應的動作方法.
正常情況下, 框架會自動創建分發器. 對我們而言, 我們想在執行請求的方法之前執行一個驗證, 校驗用戶是否可以訪問它. 要做到這一點, 我們需要在啟動文件中創建一個方法來替換組件:
~~~
<?php
use Phalcon\Mvc\Dispatcher;
// ...
/**
* MVC 分發器
*/
$di->set(
"dispatcher",
function () {
// ...
$dispatcher = new Dispatcher();
return $dispatcher;
}
);
~~~
我們現在使用完全控制的分發器用于應用程序. 在框架中需要多組件的觸發事件, 允許我們能夠修改內部的操作流. 依賴注入組件作為膠水的一部分, 一個新的叫做[EventsManager](http://docs.iphalcon.cn/reference/events.html)的組件允許我們攔截由組件產生的事件, 路由事件到監聽.
### 事件管理(Events Management)
一個[EventsManager](http://docs.iphalcon.cn/reference/events.html)允許我們為一個特定類型的事件添加監聽. 現在我們感興趣的類型是 “dispatch”. 下列代碼過濾了由分發器產生的所有事件:
~~~
<?php
use Phalcon\Mvc\Dispatcher;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// 創建一個事件管理器
$eventsManager = new EventsManager();
// 監聽分發器中使用安全插件產生的事件
$eventsManager->attach(
"dispatch:beforeExecuteRoute",
new SecurityPlugin()
);
// 處理異常和使用 NotFoundPlugin 未找到異常
$eventsManager->attach(
"dispatch:beforeException",
new NotFoundPlugin()
);
$dispatcher = new Dispatcher();
// 分配事件管理器到分發器
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
~~~
當一個叫做 “beforeExecuteRoute” 的事件觸發以下插件將會被通知:
~~~
<?php
/**
* 檢驗用戶是否允許使用 SecurityPlugin 訪問某些方法
*/
$eventsManager->attach(
"dispatch:beforeExecuteRoute",
new SecurityPlugin()
);
~~~
當一個 “beforeException” 被觸發然后其他插件通知:
~~~
<?php
/**
* 處理異常和使用 NotFoundPlugin 未找到異常
*/
$eventsManager->attach(
"dispatch:beforeException",
new NotFoundPlugin()
);
~~~
SecurityPlugin 是一個類位于(app/plugins/SecurityPlugin.php). 這個類實現了 “beforeExecuteRoute” 方法. 這是一個相同的名字在分發器中產生的事件中的一個:
~~~
<?php
use Phalcon\Events\Event;
use Phalcon\Mvc\User\Plugin;
use Phalcon\Mvc\Dispatcher;
class SecurityPlugin extends Plugin
{
// ...
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
{
// ...
}
}
~~~
鉤子事件始終接收第一個包含上下文信息所產生的事件(`$event`)的參數和第二個包含事件本身所產生的對象(`$dispatcher`)的參數. 這不是一個強制性的插件擴展類[Phalcon\\Mvc\\User\\Plugin](http://docs.iphalcon.cn/api/Phalcon_Mvc_User_Plugin.html), 但通過這樣做, 它們更容易獲得應用程序中可用的服務.
現在, 我們驗證當前 session 中的角色, 驗證用戶是否可以通過ACL列表訪問.如果用戶沒有權限, 我們將會重定向到如上所述的主頁中去:
~~~
<?php
use Phalcon\Acl;
use Phalcon\Events\Event;
use Phalcon\Mvc\User\Plugin;
use Phalcon\Mvc\Dispatcher;
class SecurityPlugin extends Plugin
{
// ...
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
{
// 檢查session中是否存在"auth"變量來定義當前活動的角色
$auth = $this->session->get("auth");
if (!$auth) {
$role = "Guests";
} else {
$role = "Users";
}
// 從分發器獲取活動的 controller/action
$controller = $dispatcher->getControllerName();
$action = $dispatcher->getActionName();
// 獲得ACL列表
$acl = $this->getAcl();
// 檢驗角色是否允許訪問控制器 (resource)
$allowed = $acl->isAllowed($role, $controller, $action);
if (!$allowed) {
// 如果沒有訪問權限則轉發到 index 控制器
$this->flash->error(
"You don't have access to this module"
);
$dispatcher->forward(
[
"controller" => "index",
"action" => "index",
]
);
// 返回 "false" 我們將告訴分發器停止當前操作
return false;
}
}
}
~~~
### 提供 ACL 列表(Providing an ACL list)
在上面的例子中我們已經獲得了ACL的使用方法`$this->getAcl()`. 這個方法也是在插件中實現的. 現在我們要逐步解釋如何建立訪問控制列表(ACL):
~~~
<?php
use Phalcon\Acl;
use Phalcon\Acl\Role;
use Phalcon\Acl\Adapter\Memory as AclList;
// 創建一個 ACL
$acl = new AclList();
// 默認行為是 DENY(拒絕) 訪問
$acl->setDefaultAction(
Acl::DENY
);
// 注冊兩個角色, 用戶是已注冊用戶和沒有定義身份的來賓用戶
$roles = [
"users" => new Role("Users"),
"guests" => new Role("Guests"),
];
foreach ($roles as $role) {
$acl->addRole($role);
}
~~~
現在, 我們分別為每個區域定義資源. 控制器名稱是資源它們的方法是對資源的訪問:
~~~
<?php
use Phalcon\Acl\Resource;
// ...
// 私有區域資源 (后臺)
$privateResources = [
"companies" => ["index", "search", "new", "edit", "save", "create", "delete"],
"products" => ["index", "search", "new", "edit", "save", "create", "delete"],
"producttypes" => ["index", "search", "new", "edit", "save", "create", "delete"],
"invoices" => ["index", "profile"],
];
foreach ($privateResources as $resourceName => $actions) {
$acl->addResource(
new Resource($resourceName),
$actions
);
}
// 公共區域資源 (前臺)
$publicResources = [
"index" => ["index"],
"about" => ["index"],
"register" => ["index"],
"errors" => ["show404", "show500"],
"session" => ["index", "register", "start", "end"],
"contact" => ["index", "send"],
];
foreach ($publicResources as $resourceName => $actions) {
$acl->addResource(
new Resource($resourceName),
$actions
);
}
~~~
ACL現在了解現有的控制器和它們相關的操作. 角色 “Users” 由權限訪問前臺和后臺的所有資源. 角色 “Guests” 僅允許訪問公共區域:
~~~
<?php
// 授權user和Grant訪問公共區域
foreach ($roles as $role) {
foreach ($publicResources as $resource => $actions) {
$acl->allow(
$role->getName(),
$resource,
"*"
);
}
}
// 授權僅角色Users 訪問私有區域
foreach ($privateResources as $resource => $actions) {
foreach ($actions as $action) {
$acl->allow(
"Users",
$resource,
$action
);
}
}
~~~
萬歲!, ACL現在終于完成了. 在下一章, 我們將會看到Phalcon中的CRUD是如何實現的并且你如何自定義它.
- 簡介
- 安裝
- 安裝(installlation)
- XAMPP下的安裝
- WAMP下安裝
- Nginx安裝說明
- Apache安裝說明
- Cherokee 安裝說明
- 使用 PHP 內置 web 服務器
- Phalcon 開發工具
- Linux 系統下使用 Phalcon 開發工具
- Mac OS X 系統下使用 Phalcon 開發工具
- Windows 系統下使用 Phalcon 開發工具
- 教程
- 教程 1:讓我們通過例子來學習
- 教程 2:INVO簡介
- 教程 3: 保護INVO
- 教程4: 使用CRUD
- 教程5: 定制INVO
- 教程 6: V?kuró
- 教程 7:創建簡單的 REST API
- 組件
- 依賴注入與服務定位器
- MVC架構
- 使用控制器
- 使用模型
- 模型關系
- 事件與事件管理器
- Behaviors
- 模型元數據
- 事務管理
- 驗證數據完整性
- Workingwith Models
- Phalcon查詢語言
- 緩存對象關系映射
- 對象文檔映射 ODM
- 使用視圖
- 視圖助手
- 資源文件管理
- Volt 模版引擎
- MVC 應用
- 路由
- 調度控制器
- Micro Applications
- 使用命名空間
- 事件管理器
- Request Environmen
- 返回響應
- Cookie 管理
- 生成 URL 和 路徑
- 閃存消息
- 使用 Session 存儲數據
- 過濾與清理
- 上下文編碼
- 驗證Validation
- 表單_Forms
- 讀取配置
- 分頁 Pagination
- 使用緩存提高性能
- 安全
- 加密與解密 Encryption/Decryption
- 訪問控制列表
- 多語言支持
- 類加載器 Class Autoloader
- 日志記錄_Logging
- 注釋解析器 Annotations Parser
- 命令行應用 Command Line Applications
- Images
- 隊列 Queueing
- 數據庫抽象層
- 國際化
- 數據庫遷移
- 調試應用程序
- 單元測試
- 進階技巧與延伸閱讀
- 提高性能:下一步該做什么?
- Dependency Injection Explained
- Understanding How Phalcon Applications Work
- Api
- Abstract class Phalcon\Acl