HardТеория4 min

Firewalls и Providers

Конфигурация firewalls, user providers, password hashers, remember me

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, метод контроллера не выполняется