nette/security

🔑 Nette 安全:通过 ACL(访问控制列表)提供身份验证、授权和基于角色的访问控制管理

安装数: 6,767,756

依赖: 298

建议者: 8

安全: 0

星标: 349

关注者: 48

分支: 40

开放问题: 12

v3.2.0 2024-01-21 21:33 UTC

README

Downloads this Month Tests Coverage Status Latest Stable Version License

简介

Nette 的身份验证和授权库。

  • 用户登录和注销
  • 验证用户权限
  • 防范漏洞
  • 如何创建自定义身份验证器和授权器
  • 访问控制列表

文档可以在网站上找到

它需要 PHP 版本 8.1 并支持 PHP 8.3。

支持我

你喜欢 Nette 安全吗?你在期待新功能吗?

Buy me a coffee

谢谢!

身份验证

身份验证意味着 用户登录,即验证用户身份的过程。用户通常使用用户名和密码来识别自己。验证由所谓的 身份验证器 完成。如果登录失败,它将抛出 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_FOUNDAuthenticator::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
}

角色

角色的目的是提供更精确的权限管理,并保持与用户名独立。用户登录后,将分配一个或多个角色。角色本身可能是简单的字符串,例如,adminmemberguest 等。它们指定在 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:可以管理文章、评论和投票

因此,我们已经定义了一些角色(guestregisteredadministrator)以及提到的资源(articlecommentspoll),用户可以访问这些资源或对它们执行操作(viewvoteaddedit)。

我们创建一个权限类的实例并定义 角色。可以使用角色的继承,这确保了例如具有 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');

继续...