nette / security
🔑 Nette 安全:通过 ACL(访问控制列表)提供身份验证、授权和基于角色的访问控制管理
Requires
- php: 8.1 - 8.3
- nette/utils: ^4.0
Requires (Dev)
- mockery/mockery: ^1.5
- nette/di: ^3.1
- nette/http: ^3.2
- nette/tester: ^2.5
- phpstan/phpstan-nette: ^1.0
- tracy/tracy: ^2.9
Conflicts
- nette/di: <3.0-stable
- nette/http: <3.1.3
This package is auto-updated.
Last update: 2024-09-18 22:10:55 UTC
README
简介
Nette 的身份验证和授权库。
- 用户登录和注销
- 验证用户权限
- 防范漏洞
- 如何创建自定义身份验证器和授权器
- 访问控制列表
文档可以在网站上找到。
它需要 PHP 版本 8.1 并支持 PHP 8.3。
支持我
你喜欢 Nette 安全吗?你在期待新功能吗?
谢谢!
身份验证
身份验证意味着 用户登录,即验证用户身份的过程。用户通常使用用户名和密码来识别自己。验证由所谓的 身份验证器 完成。如果登录失败,它将抛出 Nette\Security\AuthenticationException
。
try { $user->login($username, $password); } catch (Nette\Security\AuthenticationException $e) { $this->flashMessage('The username or password you entered is incorrect.'); }
注销用户
$user->logout();
并检查用户是否已登录
echo $user->isLoggedIn() ? 'yes' : 'no';
简单,对吧?而且所有安全方面都由 Nette 为您处理。
您还可以设置用户注销后的时间间隔(否则,用户将在会话过期时注销)。这是通过在 login()
调用之前调用的 setExpiration()
方法来完成的。将相对时间字符串作为参数指定
// login expires after 30 minutes of inactivity $user->setExpiration('30 minutes'); // cancel expiration $user->setExpiration(null);
过期时间必须设置为等于或低于会话过期时间的值。
最后一次注销的原因可以通过 $user->getLogoutReason()
方法获得,该方法返回常数 Nette\Security\User::LogoutInactivity
(如果时间已过期)或 User::LogoutManual
(当调用 logout()
方法时)。
在演示者中,您可以在 startup()
方法中验证登录
protected function startup() { parent::startup(); if (!$this->getUser()->isLoggedIn()) { $this->redirect('Sign:in'); } }
身份验证器
它是一个验证登录数据的对象,即通常的用户名和密码。简单实现是类 Nette\Security\SimpleAuthenticator,可以按这种方式定义
$authenticator = new Nette\Security\SimpleAuthenticator([ # name => password 'johndoe' => 'secret123', 'kathy' => 'evenmoresecretpassword', ]);
此解决方案更适合测试目的。我们将向您展示如何创建一个验证器,该验证器将验证凭据是否与数据库表匹配。
身份验证器是一个实现了 Nette\Security\Authenticator 接口并具有 authenticate()
方法的对象。其任务是返回所谓的 身份 或抛出异常 Nette\Security\AuthenticationException
。也可以提供更精细的错误代码 Authenticator::IDENTITY_NOT_FOUND
或 Authenticator::INVALID_CREDENTIAL
。
use Nette; class MyAuthenticator implements Nette\Security\Authenticator { private $database; private $passwords; public function __construct(Nette\Database\Context $database, Nette\Security\Passwords $passwords) { $this->database = $database; $this->passwords = $passwords; } public function authenticate($username, $password): Nette\Security\IIdentity { $row = $this->database->table('users') ->where('username', $username) ->fetch(); if (!$row) { throw new Nette\Security\AuthenticationException('User not found.'); } if (!$this->passwords->verify($password, $row->password)) { throw new Nette\Security\AuthenticationException('Invalid password.'); } return new Nette\Security\SimpleIdentity( $row->id, $row->role, // or array of roles ['name' => $row->username] ); } }
MyAuthenticator 类通过 Nette 数据库探索器 与数据库通信,并使用表 users
,其中列 username
包含用户的登录名,列 password
包含 散列。在验证用户名和密码后,它返回具有用户 ID、角色(表中的列 role
,我们将在后面提到)和包含附加数据(在我们的情况下,是用户名)的身份。
$onLoggedIn, $onLoggedOut 事件
对象 Nette\Security\User
拥有事件 $onLoggedIn
和 $onLoggedOut
,因此您可以在登录成功或用户注销后添加回调。
$user->onLoggedIn[] = function () { // user has just logged in };
身份
身份是认证器返回的关于用户的一组信息,然后存储在会话中,并通过 $user->getIdentity()
获取。因此,我们可以获取 id、角色和其他用户数据,就像我们在认证器中传递的那样。
$user->getIdentity()->getId(); // also works shortcut $user->getId(); $user->getIdentity()->getRoles(); // user data can be access as properties // the name we passed on in MyAuthenticator $user->getIdentity()->name;
重要的是,当用户注销时,身份不会被删除,仍然可用。因此,如果存在身份,它本身并不授予用户已登录的权限。如果我们想显式删除身份,可以通过 $user->logout(true)
注销用户。
因此,您仍然可以假设哪个用户在电脑上,例如,在电子商务中显示个性化优惠,但是,您只能在登录后显示他的个人数据。
身份是实现 Nette\Security\IIdentity 接口的对象,默认实现是 Nette\Security\SimpleIdentity。如前所述,身份存储在会话中,因此,例如,如果我们更改一些已登录用户的角色,旧数据将保留在身份中,直到他再次登录。
授权
授权确定用户是否有足够的权限,例如,访问特定资源或执行操作。授权假设之前的成功认证,即用户已登录。
对于非常简单的网站,例如没有区分用户权限的行政网站,可以使用已知的 isLoggedIn()
方法作为授权标准。换句话说:一旦用户登录,他就有权限执行所有操作,反之亦然。
if ($user->isLoggedIn()) { // is user logged in? deleteItem(); // if so, he may delete an item }
角色
角色的目的是提供更精确的权限管理,并保持与用户名独立。用户登录后,将分配一个或多个角色。角色本身可能是简单的字符串,例如,admin
、member
、guest
等。它们指定在 SimpleIdentity
构造函数的第二个参数中,可以是字符串或数组。
现在,我们将使用 isInRole()
方法作为授权标准,它检查用户是否具有给定的角色。
if ($user->isInRole('admin')) { // is the admin role assigned to the user? deleteItem(); // if so, he may delete an item }
如您已知,注销用户不会删除他的身份。因此,方法 getIdentity()
仍然返回对象 SimpleIdentity
,包括所有授予的角色。Nette 框架遵循“少代码,多安全”的原则,因此在检查角色时,您不需要检查用户是否已登录。方法 isInRole()
与 有效角色 一起工作,即如果用户已登录,则使用分配给身份的角色,如果未登录,则使用自动的特殊角色 guest
。
授权器
除了角色之外,我们还将引入资源和操作这两个术语。
- 角色 是用户属性 - 例如,版主、编辑、访客、注册用户、管理员等。
- 资源 是应用程序的逻辑单元 - 文章、页面、用户、菜单项、投票、表示者等。
- 操作 是特定活动,用户可以或不可以对 资源 执行 - 查看、编辑、删除、投票等。
授权器是一个对象,它决定给定的 角色 是否有权对特定的 资源 执行 操作。它是实现 Nette\Security\Authorizator 接口的对象,只有一个方法 isAllowed()
。
class MyAuthorizator implements Nette\Security\Authorizator { public function isAllowed($role, $resource, $operation): bool { if ($role === 'admin') { return true; } if ($role === 'user' && $resource === 'article') { return true; } ... return false; } }
以下是一个使用示例。请注意,这次我们调用的是方法 Nette\Security\User::isAllowed()
,而不是授权者的方法,因此没有第一个参数 $role
。此方法将依次调用 MyAuthorizator::isAllowed()
以检查所有用户角色,如果至少有一个角色有权限,则返回 true。
if ($user->isAllowed('file')) { // is user allowed to do everything with resource 'file'? useFile(); } if ($user->isAllowed('file', 'delete')) { // is user allowed to delete a resource 'file'? deleteFile(); }
两个参数都是可选的,它们的默认值意味着 一切。
权限访问控制列表(ACL)
Nette 提供了一个内置的授权器实现,即 Nette\Security\Permission 类,该类提供了一个轻量级且灵活的访问控制列表(ACL)层,用于权限和访问控制。当我们使用此类时,我们定义角色、资源和单独的权限。角色和资源可以形成层次结构。为了解释,我们将展示一个 Web 应用的示例
guest
:未登录的访客,可以阅读和浏览网站的公共部分,即阅读文章、评论投票registered
:登录用户,除了可以阅读和浏览之外,还可以发表评论administrator
:可以管理文章、评论和投票
因此,我们已经定义了一些角色(guest
、registered
和 administrator
)以及提到的资源(article
、comments
、poll
),用户可以访问这些资源或对它们执行操作(view
、vote
、add
、edit
)。
我们创建一个权限类的实例并定义 角色。可以使用角色的继承,这确保了例如具有 administrator
角色的用户可以做普通网站访客能做的事(当然更多)。
$acl = new Nette\Security\Permission; $acl->addRole('guest'); $acl->addRole('registered', 'guest'); // registered inherits from guest $acl->addRole('administrator', 'registered'); // and administrator inherits from registered
现在我们将定义用户可以访问的 资源 列表
$acl->addResource('article'); $acl->addResource('comment'); $acl->addResource('poll');
资源也可以使用继承,例如,我们可以添加 $acl->addResource('perex', 'article')
。
现在最重要的事情。我们将在它们之间定义 规则,以确定谁可以做什么
// everything is denied now // let the guest view articles, comments and polls $acl->allow('guest', ['article', 'comment', 'poll'], 'view'); // and also vote in polls $acl->allow('guest', 'poll', 'vote'); // the registered inherits the permissions from guesta, we will also let him to comment $acl->allow('registered', 'comment', 'add'); // the administrator can view and edit anything $acl->allow('administrator', $acl::All, ['view', 'edit', 'add']);
如果我们想 阻止 某人访问资源怎么办?
// administrator cannot edit polls, that would be undemocractic. $acl->deny('administrator', 'poll', 'edit');
现在我们已经创建了规则集,我们可以简单地提出授权查询
// can guest view articles? $acl->isAllowed('guest', 'article', 'view'); // true // can guest edit an article? $acl->isAllowed('guest', 'article', 'edit'); // false // can guest vote in polls? $acl->isAllowed('guest', 'poll', 'vote'); // true // may guest add comments? $acl->isAllowed('guest', 'comment', 'add'); // false
同样适用于注册用户,但他也可以评论
$acl->isAllowed('registered', 'article', 'view'); // true $acl->isAllowed('registered', 'comment', 'add'); // true $acl->isAllowed('registered', 'comment', 'edit'); // false
管理员可以编辑除投票之外的所有内容
$acl->isAllowed('administrator', 'poll', 'vote'); // true $acl->isAllowed('administrator', 'poll', 'edit'); // false $acl->isAllowed('administrator', 'comment', 'edit'); // true
权限也可以动态评估,并且我们可以将决策权交给自己的回调,将所有参数传递给它
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool { return ...; }; $acl->allow('registered', 'comment', null, $assertion);
但是,如果我们发现角色和资源的名称不足以解决问题,即我们希望定义,例如,角色 registered
只有在其是作者的情况下才能编辑资源 article
,怎么办?我们将使用对象而不是字符串,角色将是对象 Nette\Security\IRole,资源是 Nette\Security\IResource。它们的方法 getRoleId()
和 getResourceId()
将返回原始字符串
class Registered implements Nette\Security\IRole { public $id; public function getRoleId(): string { return 'registered'; } } class Article implements Nette\Security\IResource { public $authorId; public function getResourceId(): string { return 'article'; } }
现在让我们创建一个规则
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool { $role = $acl->getQueriedRole(); // object Registered $resource = $acl->getQueriedResource(); // object Article return $role->id === $resource->authorId; }; $acl->allow('registered', 'article', 'edit', $assertion);
通过传递对象来查询 ACL
$user = new Registered(...); $article = new Article(...); $acl->isAllowed($user, $article, 'edit');
一个角色可以继承自一个或多个其他角色。但是,如果一个祖先允许执行某个操作,而另一个祖先拒绝,会发生什么?这时就涉及到 角色权重 - 继承角色数组中的最后一个角色具有最大的权重,第一个角色具有最低的权重
$acl = new Nette\Security\Permission; $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', ['admin', 'guest']); $acl->isAllowed('john', 'backend'); // false // example B: role admin has greater weight than role guest $acl->addRole('mary', ['guest', 'admin']); $acl->isAllowed('mary', 'backend'); // true
角色和资源也可以被移除(removeRole()
、removeResource()
),规则也可以被撤销(removeAllow()
、removeDeny()
)。所有直接父角色的数组由 getRoleParents()
返回。是否两个实体相互继承返回 roleInheritsFrom()
和 resourceInheritsFrom()
。
多个独立认证
在一个站点和一次会话中,可以同时存在多个独立的已登录用户。例如,如果我们想对前端和后端进行单独的身份验证,我们只需为每个设置一个唯一的会话命名空间即可。
$user->getStorage()->setNamespace('forum');