Подвох 1: Порядок Firewalls -- КРИТИЧЕН
Firewalls обрабатываются сверху вниз. Первый совпавший firewall обрабатывает запрос, остальные ИГНОРИРУЮТСЯ.
# security.yaml
security:
firewalls:
# ПОРЯДОК КРИТИЧЕН!
# 1. dev firewall -- должен быть ПЕРВЫМ
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# 2. API firewall
api:
pattern: ^/api
stateless: true
custom_authenticators:
- App\Security\ApiTokenAuthenticator
# 3. Main firewall -- ПОСЛЕДНИЙ, ловит всё остальное
main:
lazy: true
form_login:
login_path: login
check_path: login
<?php
declare(strict_types=1);
// Подвох: URL /api/admin обрабатывается firewall "api", а НЕ "main"!
// Даже если в main есть form_login, /api/* уже перехвачен.
// Ещё подвох: если поменять порядок:
// main: (pattern: ^/) -- перехватит ВСЁ, включая /api!
// api: (pattern: ^/api) -- НИКОГДА не сработает!
На экзамене: Firewall с более широким pattern (
^/) должен стоять ПОСЛЕДНИМ. Если он стоит первым -- все последующие firewalls будут мертвым кодом.
Подвох 2: access_control -- First Match Wins
access_control работает по принципу "первое совпадение побеждает". Более специфичные правила должны быть ВЫШЕ.
security:
access_control:
# НЕПРАВИЛЬНЫЙ ПОРЯДОК:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/admin/super, roles: ROLE_SUPER_ADMIN }
# ^/admin/super совпадёт с ПЕРВЫМ правилом (^/admin)
# и потребует только ROLE_ADMIN вместо ROLE_SUPER_ADMIN!
# ПРАВИЛЬНЫЙ ПОРЯДОК:
- { path: ^/admin/super, roles: ROLE_SUPER_ADMIN } # Специфичное ПЕРВЫМ
- { path: ^/admin, roles: ROLE_ADMIN } # Общее ПОСЛЕДНИМ
На экзамене: access_control использует first match. Более специфичные правила (с более длинным path) ДОЛЖНЫ быть выше общих. Это одна из самых частых ошибок в конфигурации безопасности.
Подвох 3: IS_AUTHENTICATED_FULLY vs IS_AUTHENTICATED_REMEMBERED
Это НЕ одно и то же, и разница критична для безопасности.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class AccountController extends AbstractController
{
// IS_AUTHENTICATED_FULLY -- пользователь ввёл логин/пароль В ЭТОЙ сессии
#[Route('/account/change-password', name: 'change_password')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function changePassword(): Response
{
// Безопасно -- пользователь точно ввёл пароль
return new Response('Change password form');
}
// IS_AUTHENTICATED_REMEMBERED -- пользователь аутентифицирован
// через remember_me cookie (НЕ вводил пароль)
#[Route('/dashboard', name: 'dashboard')]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
public function dashboard(): Response
{
// Может быть через remember_me -- менее безопасно
return new Response('Dashboard');
}
}
Иерархия аутентификации:
IS_AUTHENTICATED_FULLY
|
v (включает)
IS_AUTHENTICATED_REMEMBERED
|
v (включает)
IS_AUTHENTICATED
|
v
PUBLIC_ACCESS
На экзамене:
IS_AUTHENTICATED_FULLYподразумеваетIS_AUTHENTICATED_REMEMBERED, но НЕ наоборот! Для критичных операций (смена пароля, удаление аккаунта) ВСЕГДА используйтеIS_AUTHENTICATED_FULLY.
Подвох 4: Remember Me Edge Cases
# security.yaml
security:
firewalls:
main:
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 неделя
always_remember_me: false
# Подвох: если always_remember_me: true,
# checkbox "remember me" в форме ИГНОРИРУЕТСЯ!
<?php
declare(strict_types=1);
// Подвох 1: Remember me cookie НЕ обновляется при каждом запросе
// Он обновляется только при аутентификации через remember_me handler
// Подвох 2: При смене пароля remember_me cookie НЕ инвалидируется
// автоматически, если не реализована проверка через signature
// Подвох 3: remember_me НЕ работает с stateless firewalls
// stateless: true означает что сессия НЕ используется
На экзамене:
always_remember_me: trueделает чекбокс в форме бесполезным -- cookie создаётся всегда. Это частый подвох в вопросах о конфигурации.
Подвох 5: Voter Strategy и приоритет
AccessDecisionManager имеет три стратегии голосования:
# security.yaml
security:
access_decision_manager:
strategy: affirmative # По умолчанию!
# Варианты: affirmative, consensus, unanimous, priority
<?php
declare(strict_types=1);
// Стратегия AFFIRMATIVE (по умолчанию):
// Хотя бы один GRANT = доступ разрешён
// Voter1: GRANT, Voter2: DENY, Voter3: ABSTAIN -> GRANTED!
// Стратегия CONSENSUS:
// Большинство GRANT = доступ разрешён
// Voter1: GRANT, Voter2: DENY, Voter3: GRANT -> GRANTED (2 > 1)
// Стратегия UNANIMOUS:
// ВСЕ должны GRANT (ABSTAIN не считается)
// Voter1: GRANT, Voter2: DENY, Voter3: GRANT -> DENIED!
// Стратегия PRIORITY:
// Первый voter, который не ABSTAIN, решает
// Voter1: ABSTAIN, Voter2: DENY, Voter3: GRANT -> DENIED!
На экзамене: По умолчанию используется
affirmativeстратегия. Это значит, что ОДИН voter с GRANT перевешивает все DENY. Многие ожидают, что DENY перевешивает -- это ошибка!
Подвох 6: Password Hasher Migration
Symfony поддерживает автоматическую миграцию хешей паролей.
# security.yaml
security:
password_hashers:
App\Entity\User:
algorithm: auto # Использует лучший доступный алгоритм
# Миграция: при логине старый хеш автоматически пересчитается
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
migrate_from:
- bcrypt # Старый алгоритм
- sha256 # Ещё более старый
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// Когда пользователь логинится с паролем, захешированным bcrypt,
// Symfony АВТОМАТИЧЕСКИ:
// 1. Проверяет пароль через bcrypt (из migrate_from)
// 2. Пересчитывает хеш через текущий algorithm (auto = argon2id)
// 3. Сохраняет новый хеш в БД
// Но ТОЛЬКО если реализован PasswordUpgraderInterface в UserRepository!
private ?string $password = null;
public function getPassword(): ?string
{
return $this->password;
}
}
На экзамене: Миграция паролей происходит прозрачно при логине. Но для сохранения нового хеша репозиторий ДОЛЖЕН реализовывать
PasswordUpgraderInterface. Без него миграция не произойдёт.
Подвох 7: Firewall и путь логина
security:
firewalls:
api:
pattern: ^/api
stateless: true
main:
form_login:
login_path: /login
check_path: /login # POST на /login
<?php
declare(strict_types=1);
// Подвох: login_path должен быть ДОСТУПЕН без аутентификации!
// Если access_control требует ROLE_USER для ^/ -- получите redirect loop:
// /login -> требует аутентификации -> redirect на /login -> ...
// ПРАВИЛЬНО:
// access_control:
// - { path: ^/login$, roles: PUBLIC_ACCESS }
// - { path: ^/, roles: ROLE_USER }
На экзамене: Путь логина (
login_path) ВСЕГДА должен быть доступен без аутентификации (PUBLIC_ACCESS). Иначе получите бесконечный redirect.
Подвох 8: Иерархия ролей
security:
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_EDITOR]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
<?php
declare(strict_types=1);
// ROLE_SUPER_ADMIN автоматически имеет:
// ROLE_ADMIN, ROLE_USER, ROLE_EDITOR, ROLE_ALLOWED_TO_SWITCH
// Подвох: getRoles() возвращает только ЯВНЫЕ роли пользователя!
// Иерархия применяется ТОЛЬКО при проверке через isGranted()
$user->getRoles(); // ['ROLE_SUPER_ADMIN'] -- без унаследованных!
$this->isGranted('ROLE_USER'); // true -- иерархия работает
На экзамене:
getRoles()возвращает только явно назначенные роли. Иерархия ролей применяется только черезisGranted(),#[IsGranted]иaccess_control. Не путайте!