# Безпека Symfony у 2026 році: Voters, Firewalls та питання технічних співбесід > Вичерпний посібник із системи безпеки Symfony: firewalls, voters, атрибут IsGranted, стратегії прийняття рішень, дебагінг у Twig 7.4 та питання технічних співбесід для PHP-розробників. - Published: 2026-06-18 - Updated: 2026-06-18 - Author: SharpSkill - Tags: symfony, security, php, voters, firewall, authentication, interview - Reading time: 13 min --- Компонент Security є одним із найскладніших і найбільш детально розглянутих елементів Symfony під час технічних співбесід для backend-розробників у 2026 році. Автентифікація, авторизація, firewalls, voters — кожен рівень спирається на точні абстракції, володіння якими відрізняє senior-спеціалістів від розробників середнього рівня. З випуском Symfony 7.4 LTS система безпеки набула кращої спостережуваності завдяки дебагінгу рішень щодо доступу безпосередньо в Twig, зберігаючи при цьому розширювану архітектуру, яка є сильною стороною фреймворку протягом багатьох років. Ця стаття глибоко аналізує внутрішні механізми системи безпеки Symfony: конфігурацію firewalls, управління токенами доступу, проєктування voters, стратегії прийняття рішень та найкращі практики зміцнення захисту. Кожен концепт проілюстровано production-ready кодом та пов'язано з питаннями, що виникають на технічних співбесідах. > **Symfony 7.4 LTS — що нового в безпеці** > > Symfony 7.4 LTS, випущений у листопаді 2025 року з підтримкою до листопада 2029, консолідує ключові зміни в компоненті Security, запроваджені починаючи з версії 6.0: уніфіковані автентифікатори, нативний атрибут IsGranted, дебагінг рішень щодо доступу в Profiler та Twig, а також вдосконалена система voters. Ця версія є точкою відліку для продакшн-проєктів і питань технічних співбесід у 2026 році. ## Firewalls: перша лінія оборони Система безпеки Symfony побудована на архітектурі firewalls, оголошених у файлі `security.yaml`. Кожен firewall визначає периметр захисту для певного набору маршрутів із власними правилами автентифікації та контролю доступу. Порядок оголошення є визначальним: Symfony проходить через firewalls послідовно і застосовує перший, патерн якого відповідає URL запиту. ```yaml # config/packages/security.yaml security: firewalls: dev: pattern: ^/(_(profiler|wdt))/ security: false api: pattern: ^/api/ stateless: true custom_authenticators: - App\Security\ApiTokenHandler main: lazy: true provider: app_user_provider form_login: login_path: app_login check_path: app_login logout: path: app_logout access_control: - { path: ^/api/public, roles: PUBLIC_ACCESS } - { path: ^/api/, roles: ROLE_API_USER } - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/dashboard, roles: ROLE_USER } ``` У цій конфігурації співіснують три firewalls. Firewall `dev` повністю вимикає безпеку для маршрутів Profiler та Web Debug Toolbar, усуваючи будь-які перешкоди в середовищі розробки. Firewall `api` захищає API-ендпоінти stateless автентифікацією на основі кастомного токена. Firewall `main` керує класичною автентифікацією через форму із сесією для веб-інтерфейсу. Секція `access_control` визначає глобальні правила доступу, що оцінюються після автентифікації. Порядок правил є критичним: перемагає перший збіг. Правило `PUBLIC_ACCESS` на `/api/public` дозволяє доступ без автентифікації, навіть коли firewall `api` активний. Ця тонкість є поширеною темою питань на співбесідах. > **Обережно з security: false на маршрутах API** > > Прапорець `security: false` повністю вимикає компонент Security для відповідних маршрутів: жодного токена, жодного користувача, жодного voter. Ця опція ніколи не повинна використовуватися на продакшн-маршрутах, відкритих для загального доступу. Для надання неавтентифікованого доступу до певних маршрутів API слід використовувати `PUBLIC_ACCESS` в `access_control`, зберігаючи активний firewall. ## Stateless проти Stateful: дві парадигми автентифікації Вибір між stateless та stateful автентифікацією визначає архітектуру безпеки застосунку. У парадигмі stateful (firewall `main`) Symfony зберігає токен автентифікації в PHP-сесії. Кожен наступний запит відновлює контекст безпеки з сесії без повторної автентифікації. Прапорець `lazy: true` оптимізує цей процес, завантажуючи сесію лише тоді, коли перевірка авторизації дійсно потрібна. У парадигмі stateless (firewall `api`) кожен запит несе власні облікові дані — Bearer-токен, API-ключ, JWT-підпис. На стороні сервера жодна сесія не створюється. Ця модель природно підходить для розподілених архітектур, мікросервісів та мобільних клієнтів, але вимагає, щоб кожен запит був самодостатнім з точки зору автентифікації. Вибір між цими двома парадигмами не є технічним уподобанням, а архітектурним обмеженням. Монолітний застосунок із веб-інтерфейсом віддає перевагу stateful-режиму через простоту управління. API, що споживається гетерогенними клієнтами, вимагає stateless-режиму для горизонтальної масштабованості. ## Access Token Handler: кастомна автентифікація Symfony 6.2 запровадив систему Access Token Handler, що уніфікує управління токенами автентифікації через чіткий інтерфейс. Цей механізм замінив старі Guard Authenticators, пропонуючи більш пряму інтеграцію з компонентом Security. ```php // src/Security/ApiTokenHandler.php namespace App\Security; use App\Repository\ApiTokenRepository; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; final readonly class ApiTokenHandler implements AccessTokenHandlerInterface { public function __construct( private ApiTokenRepository $repository, ) {} public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge { $token = $this->repository->findOneByValue($accessToken); if ($token === null || !$token->isValid()) { throw new BadCredentialsException('Invalid or expired token.'); } return new UserBadge($token->getUser()->getUserIdentifier()); } } ``` Інтерфейс `AccessTokenHandlerInterface` вимагає єдиний метод: `getUserBadgeFrom`. Цей метод отримує необроблений токен, витягнутий із запиту (за замовчуванням заголовок `Authorization: Bearer xxx`), і повинен повернути об'єкт `UserBadge`, що містить ідентифікатор пов'язаного користувача. Атрибут `#[\SensitiveParameter]` позначає токен як чутливі дані, запобігаючи його появі в stack traces та логах — передова практика безпеки, запроваджена в PHP 8.2. Патерн навмисно мінімалістичний: валідація токена (існування, термін дії, відкликання) залишається в репозиторії або окремому сервісі. Handler лише перетворює токен на ідентичність користувача. Цей поділ відповідальностей спрощує модульне тестування та дотримується принципу єдиної відповідальності. ## Voters: гранулярний контроль доступу Voters є центральним механізмом авторизації в Symfony. На відміну від статичних ролей, визначених в `access_control`, voters забезпечують динамічну логіку прийняття рішень на основі контексту: поточного користувача, цільового об'єкта та запитуваної дії. Кожен voter відповідає на точне запитання: «Чи може цей користувач виконати цю дію над цим об'єктом?» ```php // src/Security/Voter/PostVoter.php namespace App\Security\Voter; use App\Entity\Post; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Vote; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\User\UserInterface; final class PostVoter extends Voter { public const EDIT = 'POST_EDIT'; public const DELETE = 'POST_DELETE'; public const PUBLISH = 'POST_PUBLISH'; protected function supports(string $attribute, mixed $subject): bool { return in_array($attribute, [self::EDIT, self::DELETE, self::PUBLISH]) && $subject instanceof Post; } protected function voteOnAttribute( string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null, ): bool { $user = $token->getUser(); if (!$user instanceof UserInterface) { $vote?->addReason('User is not authenticated.'); return false; } /** @var Post $post */ $post = $subject; return match ($attribute) { self::EDIT => $this->canEdit($post, $user, $vote), self::DELETE => $this->canDelete($post, $user, $vote), self::PUBLISH => $this->canPublish($post, $user, $vote), default => false, }; } private function canEdit(Post $post, UserInterface $user, ?Vote $vote): bool { if ($post->getAuthor() === $user) { $vote?->addReason('User is the author of the post.'); return true; } $vote?->addReason('User is not the author.'); return false; } private function canDelete(Post $post, UserInterface $user, ?Vote $vote): bool { if (in_array('ROLE_ADMIN', $user->getRoles())) { $vote?->addReason('User has ROLE_ADMIN.'); return true; } if ($post->getAuthor() === $user && !$post->isPublished()) { $vote?->addReason('Author can delete unpublished posts.'); return true; } $vote?->addReason('Only admins or authors of unpublished posts can delete.'); return false; } private function canPublish(Post $post, UserInterface $user, ?Vote $vote): bool { if (in_array('ROLE_EDITOR', $user->getRoles())) { $vote?->addReason('User has ROLE_EDITOR.'); return true; } $vote?->addReason('Only editors can publish posts.'); return false; } } ``` Кілька елементів цього voter заслуговують на детальний аналіз. Параметр `Vote` (запроваджений у Symfony 7.1) дозволяє прикріплювати текстові обґрунтування до кожного рішення, спрощуючи дебагінг у середовищі розробки та забезпечуючи аудитованість у продакшні. Метод `supports` фільтрує виклики: voter активується виключно для атрибутів `POST_EDIT`, `POST_DELETE` та `POST_PUBLISH`, пов'язаних з екземпляром `Post`. Вираз `match` у `voteOnAttribute` делегує логіку спеціалізованим приватним методам, кожен з яких інкапсулює бізнес-правила окремої дії. Така структура робить voter читабельним та легко розширюваним: додавання нової дії зводиться до оголошення константи, додавання випадку в `match` та написання приватного методу. Логіка `canDelete` ілюструє поширений патерн: поєднання перевірки ролі з перевіркою володіння. Адміністратор може видалити будь-яку статтю, але автор може видалити лише власні неопубліковані статті. Такий тип контекстного правила неможливо виразити простими ролями в `access_control`. ## Атрибут IsGranted: декларативний контроль доступу Атрибут `#[IsGranted]` застосовує контроль доступу безпосередньо на рівні контролера або методу, виконуючи ту саму функцію, що й попередні анотації безпеки, але з нативним синтаксисом PHP. Symfony обчислює вираз перед виконанням методу і повертає відповідь 403, якщо перевірка не пройдена. ```php // src/Controller/PostController.php namespace App\Controller; use App\Entity\Post; use App\Security\Voter\PostVoter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/post')] final class PostController extends AbstractController { #[Route('/{id}/edit', methods: ['GET', 'POST'])] #[IsGranted(PostVoter::EDIT, subject: 'post', message: 'You cannot edit this post.')] public function edit(Post $post): Response { // User is guaranteed to have edit permission at this point return $this->render('post/edit.html.twig', [ 'post' => $post, ]); } #[Route('/{id}/publish', methods: ['POST'])] #[IsGranted(PostVoter::PUBLISH, subject: 'post')] public function publish(Post $post): Response { // Only editors reach this code $post->setPublished(true); // ... return $this->redirectToRoute('post_show', ['id' => $post->getId()]); } } ``` Параметр `subject: 'post'` прив'язує атрибут до параметра методу з такою самою назвою. Symfony автоматично розв'язує сутність через ParamConverter і передає її voter як суб'єкт перевірки. Параметр `message` персоналізує повідомлення про помилку 403, що корисно для дебагінгу та аудит-логів. Цей декларативний стиль має суттєву перевагу в плані читабельності: правила доступу видимі в тому самому місці, що й сигнатура методу, без необхідності перегляду тіла функції. На технічній співбесіді здатність пояснити повний шлях — від атрибута через voter до `AccessDecisionManager` — демонструє глибоке розуміння компонента Security. ## Стратегії прийняття рішень: unanimous, affirmative, consensus Коли кілька voters висловлюються щодо однієї перевірки доступу, `AccessDecisionManager` застосовує стратегію прийняття рішень для агрегації голосів. Symfony пропонує три стратегії, які налаштовуються в `security.yaml`. ```yaml # config/packages/security.yaml security: access_decision_manager: strategy: unanimous allow_if_all_abstain: false ``` Стратегія `affirmative` (за замовчуванням) надає доступ, щойно хоча б один voter проголосує позитивно. Стратегія `unanimous` вимагає, щоб усі voters, які не утрималися, проголосували позитивно. Стратегія `consensus` надає доступ, якщо більшість voters проголосували позитивно. Параметр `allow_if_all_abstain` визначає поведінку, коли всі voters утримуються. На практиці стратегія `affirmative` за замовчуванням підходить для більшості застосунків. Стратегія `unanimous` необхідна в контекстах, де безпека має пріоритет: фінансові застосунки, медичні дані, критичні системи. Вона гарантує, що жоден voter не заперечує проти доступу, додаючи шар глибинного захисту. Voter перевірки IP, voter ролі та voter володіння — усі мають схвалити доступ для його надання. ## Дебагінг рішень щодо доступу в Twig (Symfony 7.4) Symfony 7.4 збагачує дебагінг системи безпеки, відкриваючи обґрунтування рішень щодо доступу безпосередньо в шаблонах Twig. Параметр `Vote`, доданий до voters, набуває повного сенсу: обґрунтування, оголошені через `$vote->addReason()`, з'являються в Profiler і можуть умовно відображатися в шаблонах. ```twig {# templates/post/show.html.twig #} {% if is_granted('POST_EDIT', post) %} Edit {% endif %} {% if is_granted('POST_DELETE', post) %}
{% endif %} {% if app.debug %} {# Symfony 7.4: access decision debugging in Twig #} {% set decision = is_granted_debug('POST_EDIT', post) %}