Як використовувати виборців для перевірки доступів користувачів
Дата оновлення перекладу 2024-06-03
Як використовувати виборців для перевірки доступів користувачів
Виборці - це найпотужніший в Symfony спосіб управляти дозволами. Вони дозволяють вам централізувати всю логіку дозволів, а потім використовувати її повторно у багатьох місцях.
Однак, якщо ви не використовуєте дозволи повторно, або ваші правила загальні, ви завжди можете розмістити таку логіку прямо у вашому контролері. Ось приклад того, як це може виглядати, якщо ви хочете зробити маршрут доступним лише для "власника":
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, mixed $subject): bool;
abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool;
}
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
// src/Controller/PostController.php
// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;
class PostController extends AbstractController
{
#[Route('/posts/{id}', name: 'post_show')]
// перевірити на наявність доступу для "view": викликає всіх виборців
#[IsGranted('view', 'post')]
public function show(Post $post): Response
{
// ...
}
#[Route('/posts/{id}/edit', name: 'post_edit')]
// перевірити на наявність доступу для "edit": викликає всіх виборців
#[IsGranted('edit', 'post')]
public function edit(Post $post): Response
{
// ...
}
}
Метод 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
// 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;
return match($attribute) {
self::VIEW => $this->canView($post, $user),
self::EDIT => $this->canEdit($post, $user),
default => 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()
зсередини вашого виборця - наприклад, за
допомогою впровадження 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
// src/Security/PostVoter.php
// ...
use Symfony\Bundle\SecurityBundle\Security;
class PostVoter extends Voter
{
// ...
public function __construct(
private Security $security,
) {
}
protected function voteOnAttribute($attribute, mixed $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
. Ви можете встановити це
у конфігурації безпеки:
1 2 3 4 5
# config/packages/security.yaml
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
Користувацька стратегія рішення доступу
Якщо жодна із вбудованих стратегій не підходить для вашого випадку, визначте опцію
strategy_service
, щоб використати користувацький сервіс (ваш сервіс має реалізовувати
AccessDecisionStrategyInterface):
1 2 3 4 5
# config/packages/security.yaml
security:
access_decision_manager:
strategy_service: App\Security\MyCustomAccessDecisionStrategy
# ...
Користувацький менеджер рішення доступу
Якщо вам потрібно надати абсолютно користувацький менеджер рішення доступу, визначте
опцію service
, щоб використати користувацький сервіс в якості менеджера рішення
доступу (ваш сервіс має реалізовувати
AccessDecisionManagerInterface):
1 2 3 4 5
# config/packages/security.yaml
security:
access_decision_manager:
service: App\Security\MyCustomAccessDecisionManager
# ...
Зміна повідомлення та повернутого статус-коду
За замовчуванням, атрибут #[IsGranted]
буде викликати виключення
AccessDeniedException
і поверне статус-крд http 403 зі значенням Access Denied як повідомлення.
Однак, ви можете змінити цю поведінку, вказавши повідомлення і статус-код, що повертається:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Controller/PostController.php
// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;
class PostController extends AbstractController
{
#[Route('/posts/{id}', name: 'post_show')]
#[IsGranted('show', 'post', 'Post not found', 404)]
public function show(Post $post): Response
{
// ...
}
}
Tip
Якщо статус-код відрізняється від 403, то натомість буде викликано HttpException.