💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 用户授权和权限 很少甚至没有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在用户注销时调用。