# 用戶授權和權限
很少甚至沒有Web應用程序不需要用戶登錄或檢查用戶權限的機制。 在本章中,我們將討論:
* 用戶登錄和注銷
* 驗證用戶權限
* 防止漏洞
* 如何創建自定義認證和授權
* 訪問控制列表
在我們要了解這個主題之前,我們應該注意,所有示例都使用用戶服務,就是這個Nette\Security\User對象。 您可以通過調用$user=$ this-> getUser()或直接在控制器中訪問服務,或者可以使用依賴注入來請求它。
## 驗證
認證意味著用戶登錄,即。 驗證用戶身份的過程。 用戶通常使用用戶名和密碼來識別自己。
使用用戶名和密碼登錄用戶:
~~~
$user->login($username, $password);
~~~
檢查用戶是否已登錄:
~~~
echo $user->isLoggedIn() ? 'yes' : 'no';
~~~
并注銷:
~~~
$user->logout();
~~~
很簡單,對吧?
~~~
登錄需要用戶啟用Cookie - 其他方法不安全!
~~~
除了使用logout()方法注銷用戶外,還可以根據指定的時間間隔或關閉瀏覽器窗口自動完成。 對于此配置,我們必須在登錄過程中調用setExpiration()。 作為參數,它需要相對時間(以秒為單位),UNIX時間戳或時間的文本表示。 第二個參數指定在瀏覽器關閉時是否注銷用戶。
~~~
//登錄在30分鐘不活動或關閉瀏覽器后過期
$user->setExpiration('30 minutes', TRUE);
//登錄在兩天的不活動后過期
$user->setExpiration('2 days', FALSE);
// 登錄在瀏覽器關閉時過期,但不會更早(即沒有時間限制)
$user->setExpiration(0, TRUE);
~~~
~~~
到期時間必須設置為等于或低于會話的到期時間。
~~~
上面注銷的原因可以通過方法$ user-> getLogoutReason()獲得,它返回以下常量之一:IUserStorage :: INACTIVITY如果時間過期,IUserStorage :: BROWSER_CLOSED當用戶關閉瀏覽器或IUserStorage :: MANUAL時 logout()方法被調用。
為了使上面的示例工作,我們實際上必須創建一個對象來驗證用戶的名稱和密碼。 它稱為鑒別器。 它的簡單實現是Nette \ Security \ SimpleAuthenticator類,它在其構造函數中接受關聯數組:
~~~
$authenticator = new Nette\Security\SimpleAuthenticator([
'john' => 'IJ^%4dfh54*',
'kathy' => '12345', // Kathy,這是一個很弱的密碼!
]);
$user->setAuthenticator($authenticator);
~~~
如果登錄憑據無效,則驗證器拋出Nette\Security\AuthenticationException
~~~
try {
// 我們嘗試登錄用戶
$user->login($username, $password);
// ... 并重定向
$this->redirect(...);
} catch (Nette\Security\AuthenticationException $e) {
echo 'Login error: ', $e->getMessage();
}
~~~
我們通常在配置文件中配置驗證器,它僅在應用程序請求時才創建對象。 上面的示例將在config.neon中設置如下:
~~~
services:
authenticator: Nette\Security\SimpleAuthenticator([
john: IJ^%4dfh54*
kathy: 12345
])
~~~
### 自定義驗證器
我們將創建一個自定義的驗證器,它將根據數據庫表檢查登錄憑據的有效性。 每個認證器必須是Nette \ Security \ IAuthenticator的實現,其唯一的方法authenticate()。 它的唯一目的是返回一個標識或拋出一個Nette \ Security \ AuthenticationException。 框架定義了幾個錯誤代碼,可以用于確定登錄失敗的原因,如自解釋IAuthenticator :: IDENTITY_NOT_FOUND或IAuthenticator :: INVALID_CREDENTIAL。
~~~
use Nette\Security as NS;
class MyAuthenticator implements NS\IAuthenticator
{
public $database;
function __construct(Nette\Database\Connection $database)
{
$this->database = $database;
}
function authenticate(array $credentials)
{
list($username, $password) = $credentials;
$row = $this->database->table('users')
->where('username', $username)->fetch();
if (!$row) {
throw new NS\AuthenticationException('User not found.');
}
if (!NS\Passwords::verify($password, $row->password)) {
throw new NS\AuthenticationException('Invalid password.');
}
return new NS\Identity($row->id, $row->role, ['username' => $row->username]);
}
}
~~~
MyAuthenticator類使用Nette \ Database層與數據庫通信,并與users表協作,在那里它在適當的列中獲取username和bcrypt hash的password。 如果密碼檢查成功,它會返回新的身份與用戶ID,角色,我們將在后面提及一個數組與附加數據(例如用戶名)。
此驗證器將在config.neon文件中配置如下:
~~~
services:
authenticator: MyAuthenticator
~~~
### 身份
身份提供一組由授權人返回的用戶信息。 它是一個實現Nette \ Security \ IIdentity接口的對象,具有默認實現Nette \ Security \ Identity。 類具有方法getId(),它返回用戶ID(例如各個數據庫行的主鍵)和getRoles(),getRoles()返回用戶所在角色的數組。用戶數據可以像訪問標識屬性一樣訪問 。
用戶注銷時不會清除身份。 所以,如果身份存在,它本身不會授予用戶也登錄。如果我們想出于某種原因顯式刪除身份,我們通過調用$ user-> logout(TRUE)注銷用戶。
Nette \ Security \ User類的服務用戶在會話中保持身份,并將其用于所有授權。 身份可以用getIdentity訪問$ user:
~~~
if ($user->isLoggedIn()) {
echo 'User logged in: ' . $user->getIdentity()->getId();
// 或快捷方式
echo 'User logged in: ' . $user->id;
// 用戶名傳遞給身份數據
echo ' ' . $user->getIdentity()->username;
} else {
echo 'User is not logged in';
}
~~~
如前所述,身份存儲在會話中。 如果我們 改變一些登錄用戶的角色,舊數據將保存在身份中,直到他再次登錄。
## 授權
授權檢測用戶是否有足夠的權限來執行某些操作,例如打開文件或刪除文章。 授權假定用戶已成功通過身份驗證(登錄)。
Nette框架授權可以基于用戶屬于什么組或者角色被分配給用戶。 我們將從一開始就開始。
對于具有管理功能的簡單Web站點,其中所有用戶共享相同的權限,使用已經提到的isLoggedIn()方法就足夠了。 簡單地說,如果用戶登錄,他具有所有操作的權限,反之亦然。
~~~
if ($user->isLoggedIn()) { // 是否用戶登錄?
deleteItem(); //如果是,他可以刪除項目
}
~~~
## 角色
角色的目的是提供更精確的特權控制,同時保持獨立于用戶名。 一旦用戶登錄,他將被分配一個或多個角色。 角色本身可以是簡單的字符串,例如admin,member,guest等。它們在Identity構造函數的第二個參數中指定為字符串或數組。
這次我們將使用isInRole()方法來檢查用戶是否被允許執行一些操作:
~~~
if ($user->isInRole('admin')) { //是分配給用戶的管理角色?
deleteItem(); // 如果是,他可以刪除項目
}
~~~
正如你已經知道的,記錄用戶不會抹去他的身份。 因此,getIdentity()方法仍然返回Identity對象,其中包含所有分配的角色,而不考慮注銷。 Nette Framework堅持“少代碼,更安全”的原則,這就是為什么它不想強迫編碼器寫if($ user-> isLoggedIn()&& $ user-> isInRole('admin'))無處不在, 因此isInRole()方法使用efective角色。 如果用戶登錄,則使用分配給身份的角色,如果用戶已注銷,則使用自動特殊角色guest。
## 授權人
授權者決定用戶是否有權采取某些操作。 它是一個Nette \ Security \ IAuthorizator接口的實現,只有一個方法isAllowed()。 此方法的目的是確定給定角色是否具有對特定資源執行某些操作的權限。
* role -是用戶屬性 - 例如主持人,編輯者,訪問者,注冊用戶,管理員...
* resource-是應用程序的邏輯單元 - 文章,頁面,用戶,菜單項,投票,演示者,...
* privilege-是一個特定的活動,用戶可能或可能不會做資源 - 查看,編輯,刪除,投票,...
實現框架如下所示:
~~~
class MyAuthorizator implements Nette\Security\IAuthorizator
{
function isAllowed($role, $resource, $privilege)
{
return ...; // 返回TRUE或FALSE
}
}
~~~
和一個使用示例:
~~~
//注冊授權人
$user->setAuthorizator(new MyAuthorizator);
if ($user->isAllowed('file')) { //是用戶允許做的一切與資源'文件'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { //用戶是否允許刪除資源“文件”?
deleteFile();
}
~~~
~~~
不要混淆兩種不同的方法isAllowed:一個屬于authorizator,另一個屬于User類,其中第一個參數不是$ role。
~~~
因為用戶可能具有許多角色,所以只有當至少一個角色具有該權限時才授予他的權限。 這兩個參數是可選的,它們的默認值是一切。
## 權限ACL
Nette Framework有一個完整的授權者,類Nette \ Security \ Permission它提供了一個輕量級和靈活的ACL層的權限和訪問控制。 當我們使用這個類時,我們定義角色,資源和個人特權。 角色和資源可能形成層次結構,如以下示例所示:
guest:未登錄的訪問者,允許讀取和瀏覽web的公共部分,即。 文章,評論,并在投票中投票
registered:登錄的用戶,這可能在該帖的評論
administrator:可以寫和管理文章,評論和投票
因此,我們定義了某些角色(訪客,注冊和管理員)和提到的資源(文章,評論,投票),用戶可以訪問或執行操作(查看,投票,添加,編輯)。
我們創建一個Presmission的實例并定義用戶角色。 由于角色可以彼此繼承,我們可以例如指定管理員可以做與普通訪問者相同的(當然更多)。
~~~
$acl = new Nette\Security\Permission;
// 角色定義
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); //注冊繼承自guest
$acl->addRole('administrator', 'registered'); // 而管理員繼承自注冊
~~~
瑣碎,不是嗎? 這確保父母的所有屬性將由他們的孩子繼承。
請記住方法getRoleParents(),它返回所有直接父角色的數組,以及方法roleIntheritsFrom(),它檢查角色是否擴展了另一個角色。 他們的用法:
~~~
$acl->roleInheritsFrom('administrator', 'guest'); // TRUE
$acl->getRoleParents('administrator'); // ['registered'] - only direct parents
~~~
現在是定義用戶可能接受的資源集合的正確時間:
~~~
$acl->addResource('article');
$acl->addResource('comments');
$acl->addResource('poll');
~~~
資源也可以使用繼承。 API提供類似的方法,只有名稱稍有不同:resourceInheritsFrom(),removeResource()。
而現在最重要的部分。 角色和資源本身不會使我們失望,我們必須創建規則,定義誰可以做什么與任何:
~~~
// 一切都被拒絕了
// 客人可以查看文章,評論和投票
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
$acl->allow('guest', 'poll', 'vote');
// 注冊用戶也有權添加評論
$acl->allow('registered', 'comment', 'add');
//管理員還可以編輯和添加所有內容
$acl->allow('administrator', Permission::ALL, ['view', 'edit', 'add']);
//管理員不能編輯投票,這將是不民主的。
$acl->deny('administrator', 'poll', 'edit');
~~~
如果我們想阻止某人**特定的資源訪問**怎么辦?
~~~
//管理員看不到廣告
$acl->deny('administrator','ad','view');
~~~
現在當我們創建規則集時,我們可以簡單地詢問授權查詢:
~~~
// 可以訪客查看文章?
echo $acl->isAllowed('guest', 'article', 'view'); // TRUE
//客人可以編輯文章嗎?
echo $acl->isAllowed('guest', 'article', 'edit'); // FALSE
// 可能客人添加評論?
echo $acl->isAllowed('guest', 'comments', 'add'); // FALSE
~~~
注冊用戶也是如此,盡管他可以添加評論:
~~~
echo $acl->isAllowed('registered', 'article', 'view'); // TRUE
echo $acl->isAllowed('registered', 'comments', 'add'); // TRUE
echo $acl->isAllowed('registered', 'backend', 'view'); // FALSE
~~~
管理員允許做任何事情,除了編輯投票:
~~~
echo $acl->isAllowed('administrator', 'article', 'view'); // TRUE
echo $acl->isAllowed('administrator', 'commend', 'add'); // TRUE
echo $acl->isAllowed('administrator', 'poll', 'edit'); // FALSE
~~~
管理規則可能沒有任何限制地定義(不繼承任何其他角色):
~~~
$acl->addRole('supervisor');
$acl->allow('supervisor'); // 所有資源的所有權限為主管
~~~
每當在應用程序運行時,我們可以使用removeRolle(),removeResource()或removeAllow()或removeDeny()刪除角色。
角色可以繼承一個或多個其他角色。 但是,如果一個祖先允許某些行為,而另一個祖先被拒絕,會發生什么? 然后角色權重發揮作用 - 角色數組中的最后一個角色具有最大的權重,第一個最低的權重:
~~~
$acl = new Permission();
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// 示例A:角色管理員的角色權重低于角色guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // FALSE
// 示例B:角色管理員比角色guest具有更大的權重
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // TRUE
~~~
## 在應用程序中的用法
我們可以在config.neon中這樣配置權限:
~~~
services:
acl:
class: Nette\Security\Permission
setup:
- addRole(admin)
- addRole(guest)
- addResource(backend)
- allow(admin, backend)
- deny(guest, backend)
# example A: role admin has lower weight than role guest
- addRole(john, [admin, guest])
# example B: role admin has greater weight than role guest
- addRole(mary, [guest, admin])
~~~
然后我們可以驗證控制器中的權限 在啟動方法:
~~~
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
throw new Nette\Application\ForbiddenRequestException;
}
}
~~~
以下解決方案是上一個解決方案的替代。 我們創建工廠服務,在那里我們可以設置權限:
~~~
<?php
namespace App\Model;
use Nette;
class AuthorizatorFactory
{
/** @return Nette\Security\Permission */
public static function create()
{
$acl = new Nette\Security\Permission;
//if we want, we can load roles from database
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// example A: role admin has lower weight than role guest
$acl->addRole('john', array('admin', 'guest'));
$acl->isAllowed('john', 'backend'); // FALSE
// example B: role admin has greater weight than role guest
$acl->addRole('mary', array('guest', 'admin'));
$acl->isAllowed('mary', 'backend'); // TRUE
return $acl;
}
}
~~~
然后我們必須注冊工廠到config.neon并使用它作為工廠Permission:
~~~
acl: App\Model\AuthorizatorFactory::create #here we specify, that AuthorizationFactory will be factory for Permission
~~~
## 應用程序中的多個身份驗證
應用程序(服務器,會話)也可以分成多個獨立的段,每個段都有獨立的認證邏輯。 例如,如果我們希望有前端和后端,每個都有單獨的認證,我們將為它們中的每一個設置一個唯一的命名空間:
~~~
$user->getStorage()->setNamespace('backend');
~~~
有必要記住,這必須在屬于同一段的所有地方設置。 當使用控制器時,我們將在共同的祖先中設置命名空間 - 通常是BasePresenter。 為了這樣做,我們將擴展checkRequirements()方法:
~~~
public function checkRequirements($element)
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
~~~
### 事件:onLoggedIn,onLoggedOut
用戶服務提供事件:onLoggedIn和onLoggedOut,用于記錄網站上的授權活動。 onLoggedIn事件僅在用戶已成功登錄時調用,另一個onLoggedOut在用戶注銷時調用。
- Nette簡介
- 快速開始
- 入門
- 主頁
- 顯示文章詳細頁
- 文章評論
- 創建和編輯帖子
- 權限驗證
- 程序員指南
- MVC應用程序和控制器
- URL路由
- Tracy - PHP調試器
- 調試器擴展
- 增強PHP語言
- HTTP請求和響應
- 數據庫
- 數據庫:ActiveRow
- 數據庫和表
- Sessions
- 用戶授權和權限
- 配置
- 依賴注入
- 獲取依賴關系
- DI容器擴展
- 組件
- 字符串處理
- 數組處理
- HTML元素
- 使用URL
- 表單
- 驗證器
- 模板
- AJAX & Snippets
- 發送電子郵件
- 圖像操作
- 緩存
- 本土化
- Nette Tester - 單元測試
- 與Travis CI的持續集成
- 分頁
- 自動加載
- 文件搜索:Finder
- 原子操作