Firewall
Firewall -- точка входа системы безопасности. Каждый HTTP-запрос проходит через один firewall, который определяет, как аутентифицировать пользователя.
Конфигурация Firewall
# config/packages/security.yaml
security:
firewalls:
# Dev firewall -- no security for dev tools
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# API firewall -- stateless
api:
pattern: ^/api
stateless: true
custom_authenticators:
- App\Security\ApiTokenAuthenticator
# Main firewall -- form login
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
target: app_home
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week
path: /
custom_authenticators:
- App\Security\LoginFormAuthenticator
Параметры Firewall
| Параметр | Описание |
|---|---|
pattern |
Regex для URL, который обрабатывает этот firewall |
security |
Включить/выключить безопасность |
stateless |
Не использовать сессии (для API) |
lazy |
Аутентификация только при необходимости |
provider |
User provider для этого firewall |
custom_authenticators |
Список аутентификаторов |
Подвох экзамена: Разница между stateless и stateful firewalls критически важна. Stateless firewall НЕ создаёт сессию -- каждый запрос аутентифицируется заново (через токен, API key и т.д.). Stateful firewall сохраняет аутентификацию в сессии -- после первого входа пользователь остаётся залогиненным.
Stateless vs Stateful
security:
firewalls:
# STATELESS -- for API
# No session, authenticate on every request
api:
pattern: ^/api
stateless: true
custom_authenticators:
- App\Security\ApiTokenAuthenticator
# STATEFUL -- for web interface
# Session stores authentication token
main:
lazy: true
form_login:
login_path: app_login
check_path: app_login
| Аспект | Stateless | Stateful |
|---|---|---|
| Сессия | Нет | Да |
| Каждый запрос | Аутентификация заново | Из сессии |
| Использование | REST API, webhook | Веб-интерфейс |
| Remember Me | Нет | Да |
| CSRF | Обычно нет | Обязательно |
| Logout | Нет необходимости | Нужен endpoint |
Подвох экзамена: Если firewall stateless, то
remember_meиlogoutне имеют смысла и не будут работать. На экзамене могут показать конфигурацию сstateless: trueиremember_me-- это некорректная конфигурация.
User Providers
User Provider -- источник данных о пользователях. Отвечает за загрузку пользователя по идентификатору.
Entity Provider (Doctrine)
security:
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email # Field used as identifier
Memory Provider (для тестов)
security:
providers:
in_memory:
memory:
users:
admin:
password: '$2y$13$...'
roles: ['ROLE_ADMIN']
user:
password: '$2y$13$...'
roles: ['ROLE_USER']
Chain Provider
security:
providers:
# Try multiple providers
all_users:
chain:
providers: ['app_user_provider', 'admin_provider']
app_user_provider:
entity:
class: App\Entity\User
property: email
admin_provider:
entity:
class: App\Entity\Admin
property: username
Custom User Provider
<?php
declare(strict_types=1);
namespace App\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* @implements UserProviderInterface<User>
*/
final class ApiUserProvider implements UserProviderInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient,
) {
}
public function loadUserByIdentifier(string $identifier): UserInterface
{
$response = $this->httpClient->request('GET', '/api/users', [
'query' => ['email' => $identifier],
]);
$data = $response->toArray();
if (empty($data)) {
throw new UserNotFoundException(
sprintf('User "%s" not found.', $identifier)
);
}
return new User(
email: $data['email'],
roles: $data['roles'],
password: $data['password'],
);
}
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', $user::class)
);
}
return $this->loadUserByIdentifier($user->getUserIdentifier());
}
public function supportsClass(string $class): bool
{
return User::class === $class;
}
}
Remember Me
Remember Me позволяет пользователю оставаться аутентифицированным между сессиями браузера.
security:
firewalls:
main:
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
always_remember_me: false
token_provider:
doctrine: true # Store tokens in database
<?php
declare(strict_types=1);
// In authenticator -- add RememberMeBadge
public function authenticate(Request $request): Passport
{
return new Passport(
new UserBadge($email),
new PasswordCredentials($password),
[
new RememberMeBadge(), // Enable remember me
]
);
}
В шаблоне:
<form method="post" action="{{ path('app_login') }}">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
<button type="submit">Login</button>
</form>
Logout
security:
firewalls:
main:
logout:
path: app_logout
target: app_home # Redirect after logout
invalidate_session: true
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\Routing\Attribute\Route;
final class SecurityController extends AbstractController
{
#[Route('/logout', name: 'app_logout')]
public function logout(): never
{
// This method is intercepted by the logout handler
// It will never be executed
throw new \LogicException(
'This should never be reached!'
);
}
}
Logout Events
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
final class LogoutSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [LogoutEvent::class => 'onLogout'];
}
public function onLogout(LogoutEvent $event): void
{
$user = $event->getToken()?->getUser();
// Log the logout
// Clear custom cookies
// Invalidate API tokens
$response = $event->getResponse();
$response->headers->clearCookie('custom_cookie');
}
}
Полный пример конфигурации безопасности
# config/packages/security.yaml
security:
# Password hashing
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# User providers
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
# Firewalls
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api
stateless: true
custom_authenticators:
- App\Security\ApiTokenAuthenticator
main:
lazy: true
provider: app_user_provider
custom_authenticators:
- App\Security\LoginFormAuthenticator
form_login:
login_path: app_login
check_path: app_login
default_target_path: app_home
enable_csrf: true
logout:
path: app_logout
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
# Role hierarchy
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
# Access control
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
- { path: ^/login, roles: PUBLIC_ACCESS }
Итоги
- Firewall определяет, как аутентифицировать пользователя для группы URL
- Stateless не использует сессии (для API), stateful сохраняет в сессии
- User Provider загружает пользователя по идентификатору
- Entity Provider -- Doctrine, Memory Provider -- для тестов
- Remember Me сохраняет аутентификацию между сессиями
- Logout обрабатывается автоматически через firewall, метод контроллера не выполняется