Как использовать избирателей для проверки разрешений пользователей
Дата обновления перевода 2023-01-11
Как использовать избирателей для проверки разрешений пользователей
Избиратели - это наиболее мощный в Symfony способ управлять разрешениями. Они позволяют вам централизовать всю логику разрешений, а затем использовать ее повторно во множестве мест.
Однако, если вы не используете разрешения повторно, или ваши правила общие, вы всегда можете разместить такую логику прямо в вашем контроллере. Вот пример того, как это может выглядеть, если вы хотите сделать маршрут доступным только для "владельца" (owner):
1 2 3 4 5 6 7
// src/Controller/PostController.php
// ...
// внутри вашего действия контроллера
if ($post->getOwner() !== $this->getUser()) {
throw $this->createAccessDeniedException();
}
В таком смысле, следующий пример, используемый далее в этой статье, является минимумом для избирателей.
Вот, как Symfony работает с избирателями:
Все избиратели вызываются каждый раз, кога вы используете метод isGranted()
в проверке авторизации Symfony или вызываете denyAccessUnlessGranted()
в контроллере
(который использует проверку авторизации), или через контроль доступа .
В конечном счёте, Symfony берёт ответы от всех избирателей и принимает окончательное решение (о том, чтобы разрешить или запретить доступ к ресурсу) в соответствии со стратегией, определённой в приложении, которая может быть: утвердительной, консенсусной или единогласной.
Интерфейс избирателя
Пользовательский избиратель должен реализовывать VoterInterface или расширять Voter, что делает создание избирателя ещё легче:
1 2 3 4 5 6 7 8
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
abstract class Voter implements VoterInterface
{
abstract protected function supports(string $attribute, $subject);
abstract protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token);
}
Tip
Проверка каждого избирателя несколько раз может требовать очень много времени для приложений, выполняющих множество проверок разрешений. Чтобы улучшить производительность в таких случаях, вы можете сделать так, чтобы ваши избиратели реализовывали CacheableVoterInterface. Это предоставляет доступ к менеджеру решений, чтобы запомнить атрибут и тип субъекта, поддерживаемого избирателем, для того, чтобы вызывать только необходимых избирателей каждый раз.
Установка: Проверка доступа в контроллере
Представьте, что у вас есть объект Post
и вам нужно решить, может ли текущий
пользователь редактировать или просматривать объект. В вашем контроллере, вы
проверите доступ со следующим кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// src/Controller/PostController.php
// ...
class PostController extends AbstractController
{
#[Route('/posts/{id}', name: 'post_show')]
public function show($id): Response
{
// получить объект Post - например, запросить его
$post = ...;
// проверить разрешение "просмотра": вызов всех избирателей
$this->denyAccessUnlessGranted('view', $post);
// ...
}
#[Route('/posts/{id}/edit', name: 'post_edit')]
public function edit($id): Response
{
// получить объект Post - например, запросить его
$post = ...;
// проверить разрешение "редактирования": вызов всех избирателей
$this->denyAccessUnlessGranted('edit', $post);
// ...
}
}
Метод denyAccessUnlessGranted()
(а также метод isGranted()
) делает вызов к
системе "избирателей". Сейчас, ни один избиратель не проголосует о том, может ли
пользователь "просматривать" или "редактировать" Post
. Но вы можете создать вашего
собственного избирателя, который решает это, используя любую желаемую вами логику.
Создание пользовательского избирателя
Представьте, что логика для решения, может ли пользователь "просматривать" или
"редактировать" объект Post
, достаточно сложная. Например, User
может всегда
просматривать или редактировать Post
, который он создал. А если Post
отмечен, как "публичный", то его может просмотреть кто угодно. Избиратель для
этой ситуации будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
// src/Security/PostVoter.php
namespace App\Security;
use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class PostVoter extends Voter
{
// эти строки были просто выдуманы: вы можете использовать что угодно
const VIEW = 'view';
const EDIT = 'edit';
protected function supports(string $attribute, $subject): bool
{
// если это не один из поддерживаемых атрибутов, возвращается false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
// голосовать только по объектам Post внутри этого избирателя
if (!$subject instanceof Post) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
// пользователь должен быть в системе; если нет - отказать в доступе
return false;
}
// вы знаете, что $subject - это объект Post, благодаря поддержке
/** @var Post $post */
$post = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($post, $user);
case self::EDIT:
return $this->canEdit($post, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canView(Post $post, User $user): bool
{
// если они могут просматривать, то они могут редактировать
if ($this->canEdit($post, $user)) {
return true;
}
// обьект Post может иметь, например, метод isPrivate(),
return !$post->isPrivate();
}
private function canEdit(Post $post, User $user): bool
{
// предполагает, что объект данных имеет метод getOwner(),
return $user === $post->getOwner();
}
}
Вот и всё! Избиратель готов! Далее, сконфигурируйте его .
Чтобы подытожить, вот то, что ожидается от двух абстрактных методов:
Voter::supports(string $attribute, $subject)
-
Когда вызывается
isGranted()
(илиdenyAccessUnlessGranted()
), первый аргумент передаётся как$attribute
(например,ROLE_USER
,edit
), а второй аргумент (если он есть) - как$subject
(например,null
, объектPost
). Ваша задача - определить, должен ли ваш избиратель голосовать по комбинации атрибут/субъект. Если вы вернёте "true", тоvoteOnAttribute()
будет вызван. В обратном случае, ваш избиратель закончил: какой-то другой избиратель должен это обработать. В этом примере, вы возвращаетеtrue
, если атрибут -view
илиedit
, и если объект - экземплярPost
. voteOnAttribute(string $attribute, $subject, TokenInterface $token)
-
Если вы возвращаете
true
изsupports()
, то вызывается этот метод. Ваша задача проста: вернутьtrue
, чтобы разрешить доступ, иfalse
, чтобы его запретить.$token
может быть использован, чтобы найти текущий объект пользователя (если он есть). В этомпримере, вся сложная бизнес-логика включена для того, чтобы определить доступ.
Конфигурация избирателя
Чтобы внедрить избирателя в слой безопасности, вы должны объявить его, как сервис
и тегировать его с помощью security.voter
. Но если вы используете
конфигурацию services.yml по умолчанию ,
то это делается за вас автоматически! Когда вы
вызываете isGranted() с просмотром/редактированием и передаёте объект Post ,
ваш избиратель будет выполнен и вы сможете контролировать доступ.
Проверка ролей внутри избирателя
Что, если вы хотите вызвать isGranted()
изнутри вашего избирателя - например,
вы хотите увидеть, имеет ли текущий пользователь ROLE_SUPER_ADMIN
. Это возможно
с помощью внедрения AccessDecisionManager
в вашего избирателя. Вы можете использовать это для того, чтобы, к примеру, всегда
разрешать доступ пользователю с ROLE_SUPER_ADMIN
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// src/Security/PostVoter.php
// ...
use Symfony\Component\Security\Core\Security;
class PostVoter extends Voter
{
// ...
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
// ...
// ROLE_SUPER_ADMIN может сделать что угодно! Вот это сила!
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
return true;
}
// ... вся логика нормального избирателя
}
}
Если вы используете конфигурацию services.yml по умолчанию ,
то вы закончили! Symfony автоматически передаст сервис security.helper
при инстанциировании вашего избирателя (благодаря автомонтированию).
Изменение стратегии решений доступа
Обычно, один избиратель буде голосовать в любое данное время (а остальные будут
"воздерживаться", что означает, что они вернут false
из supports()
). Но
в теории, вы можете заставить несколько избирателей голосоватьпо одному действию
и объекту. Например, представьте, что у вас есть один избиратель, который проверяет,
является ли пользователь членом этого сайта, и второй, который проверяет, чтобы
возраст пользователя был старше 18 лет.
Чтобы обработать эти случаи, менеджер решений доступа использует "стратегию", которую вы можете сконфигурировать. Существует три доступные стратегии:
affirmative
(по умолчанию)- Гарантирует доступ, как только есть один избиратель, гарантирующий доступ;
consensus
-
Гарантирует доступ, если больше избирателей гарантируют доступ, чем отказывают в нём.
В случае ничьей, решение основывается на опции конфигурации
allow_if_equal_granted_denied
(по умолчаниюtrue
); unanimous
- Гарантирует доступ только, если нет избирателей, запрещающих доступ.
priority
- Гарантирует или отказываетв доступе по первому избирателю, который не удерживается, основываясь на его приоритетности сервисов;
Независимо от выбранной стратегии, если все избиратели удерживаются от голосования,
решение основывается на опции конфигурации allow_if_all_abstain
(по умолчанию
false
).
В вышеописанном сценарии, оба избирателя должны гарантировать доступ, чтобы
гарантировать пользователю доступ к чтению записи. В этом случае, стратегия
по умолчанию не валидна, и вместо неё должна быть использована unanimous
.
Вы можете установить это в конфигурации безопасности:
- YAML
- XML
- PHP
1 2 3 4 5
# config/packages/security.yaml
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
Пользовательская стратегия решений доступа
Если ни одна из встроенных стратегий вам не подходит, определите опцию
strategy_service
, чтобы использовать пользовательский сервис (ваш
сервис должен реализовывать AccessDecisionStrategyInterface):
- YAML
- XML
- PHP
1 2 3 4 5
# config/packages/security.yaml
security:
access_decision_manager:
strategy_service: App\Security\MyCustomAccessDecisionStrategy
# ...
Пользовательский менеджер решения доступа
Если вам нужно предоставить абсолютно пользовательский менеджер решения доступа,
определеите опцию service
, чтобы использовать пользовательский сервис в качестве
Менеджера решения доступа (ваш сарвис должен реализовывать AccessDecisionManagerInterface):
- YAML
- XML
- PHP
1 2 3 4 5
# config/packages/security.yaml
security:
access_decision_manager:
service: App\Security\MyCustomAccessDecisionManager
# ...