Segurança no Symfony em 2026: Voters, Firewalls e Perguntas de Entrevista Técnica

Análise aprofundada da segurança no Symfony cobrindo voters, firewalls, access tokens e autenticação, com as perguntas de entrevista técnica mais frequentes para desenvolvedores Symfony.

Arquitetura de segurança Symfony com voters, firewalls e autenticação

O sistema de segurança do Symfony opera por meio de dois mecanismos centrais: os firewalls gerenciam a autenticação (quem é o usuário?) e os voters gerenciam a autorização (o que o usuário pode fazer?). Compreender como essas peças interagem é fundamental para construir aplicações Symfony seguras e para responder com confiança às perguntas de entrevistas técnicas.

Stack de segurança do Symfony 7.4 LTS

O Symfony 7.4 (a versão LTS atual, com suporte até novembro de 2029) introduziu explicações para decisões de voters, novas funções Twig de autorização (access_decision() e access_decision_for_user()) e assinatura de mensagens para handlers do Messenger. Todos os exemplos neste artigo são direcionados ao Symfony 7.4+.

Como os Firewalls do Symfony controlam a autenticação

Um firewall no Symfony não é um firewall de rede. Trata-se do ponto de entrada do sistema de autenticação, definido em config/packages/security.yaml. Cada firewall declara qual parte da aplicação ele protege e qual mecanismo de autenticação utiliza.

A ordem dos firewalls é determinante. O Symfony os avalia de cima para baixo e direciona cada requisição para o primeiro firewall cujo pattern corresponda. Uma configuração típica separa os endpoints de API das rotas web:

yaml
# config/packages/security.yaml
security:
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern: ^/api
            stateless: true
            access_token:
                token_handler: App\Security\ApiTokenHandler

        main:
            lazy: true
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout
            remember_me:
                secret: '%kernel.secret%'
            login_throttling:
                max_attempts: 5
                interval: '15 minutes'

O firewall dev desativa a segurança completamente para as rotas do profiler e dos assets. O firewall api utiliza stateless: true porque os clientes de API enviam um access token em cada requisição em vez de depender de sessões. O firewall main gerencia a autenticação baseada em navegador com formulário de login, proteção CSRF, cookies remember-me e limitação de tentativas de login.

Firewalls Stateless vs Stateful e quando usar cada um

Os firewalls stateful (o comportamento padrão) armazenam o usuário autenticado na sessão. Essa abordagem é adequada para aplicações web tradicionais onde o navegador envia cookies em cada requisição. A opção lazy: true adia o carregamento do usuário a partir da sessão até que a autorização realmente o exija, melhorando o desempenho em páginas públicas.

Os firewalls stateless (stateless: true) nunca leem nem escrevem sessões. Cada requisição deve carregar suas próprias credenciais, geralmente um JWT ou token de API no header Authorization. Essa é a abordagem padrão para APIs REST, backends mobile e comunicação entre microsserviços.

Combinar ambos na mesma aplicação é comum. A configuração acima demonstra esse padrão: consumidores de API se autenticam via tokens, enquanto usuários humanos se autenticam via formulário de login com persistência em sessão.

Construindo um Access Token Handler personalizado

O Symfony 7.4 fornece um sistema integrado de autenticação por access token. Para tokens de API armazenados no banco de dados, basta implementar AccessTokenHandlerInterface:

src/Security/ApiTokenHandler.phpphp
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;

class ApiTokenHandler implements AccessTokenHandlerInterface
{
    public function __construct(
        private readonly ApiTokenRepository $repository,
    ) {}

    public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
    {
        $token = $this->repository->findOneByValue($accessToken);

        if ($token === null || $token->isExpired()) {
            throw new BadCredentialsException('Invalid or expired API token.');
        }

        return new UserBadge($token->getUser()->getUserIdentifier());
    }
}

O atributo #[\SensitiveParameter] impede que o valor do token apareça em stack traces e dumps de depuração. O handler recebe o token bruto a partir do header Authorization: Bearer <token>, busca-o no banco de dados e retorna um UserBadge que resolve o usuário associado.

Voters: autorização granular baseada em lógica de negócio

Os roles (ROLE_ADMIN, ROLE_EDITOR) funcionam para permissões estáticas. Para autorização dinâmica dependente de contexto — "este usuário pode editar este post específico?" — os voters são a solução.

Um voter é uma classe PHP que implementa VoterInterface e decide conceder, negar ou se abster em uma verificação de permissão:

src/Security/Voter/PostVoter.phpphp
namespace App\Security\Voter;

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
{
    public const EDIT = 'POST_EDIT';
    public const DELETE = 'POST_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE])
            && $subject instanceof Post;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        return match ($attribute) {
            self::EDIT => $this->canEdit($post, $user),
            self::DELETE => $this->canDelete($post, $user),
            default => false,
        };
    }

    private function canEdit(Post $post, User $user): bool
    {
        return $post->getAuthor() === $user;
    }

    private function canDelete(Post $post, User $user): bool
    {
        return $post->getAuthor() === $user
            || in_array('ROLE_ADMIN', $user->getRoles());
    }
}

Graças à autoconfiguração do Symfony, essa classe é automaticamente detectada e registrada como voter. Nenhuma configuração adicional é necessária.

Usando o atributo #[IsGranted] em controllers

O atributo #[IsGranted] permite declarar os requisitos de autorização diretamente nos métodos do controller, eliminando as chamadas manuais a $this->denyAccessUnlessGranted():

src/Controller/PostController.phpphp
namespace App\Controller;

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class PostController extends AbstractController
{
    #[Route('/post/{id}/edit', name: 'post_edit')]
    #[IsGranted(PostVoter::EDIT, subject: 'post')]
    public function edit(Post $post): Response
    {
        // A autorização já foi verificada — o código do controller fica limpo
        return $this->render('post/edit.html.twig', [
            'post' => $post,
        ]);
    }

    #[Route('/post/{id}', name: 'post_delete', methods: ['DELETE'])]
    #[IsGranted(PostVoter::DELETE, subject: 'post')]
    public function delete(Post $post): Response
    {
        // Lógica de exclusão
        return $this->redirectToRoute('post_list');
    }
}

O parâmetro subject corresponde ao nome do argumento do controller. O Symfony resolve automaticamente a entidade Post a partir do parâmetro de rota {id} e a passa para o voter. Se o voter negar o acesso, uma AccessDeniedException é lançada antes que o corpo do método seja executado.

Decisões de Voters com explicações (Symfony 7.3+)

A partir do Symfony 7.3, os voters podem fornecer explicações legíveis para suas decisões por meio do parâmetro ?Vote $vote:

src/Security/Voter/DocumentVoter.phpphp
namespace App\Security\Voter;

use App\Entity\Document;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class DocumentVoter extends Voter
{
    public const VIEW = 'DOCUMENT_VIEW';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return $attribute === self::VIEW && $subject instanceof Document;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token,
        ?Vote $vote = null,
    ): bool {
        $user = $token->getUser();

        if (!$user instanceof User) {
            $vote?->addReason('User is not authenticated.');
            return false;
        }

        /** @var Document $document */
        $document = $subject;

        if ($document->isPublic()) {
            $vote?->addReason('Document is public — access granted to all authenticated users.');
            return true;
        }

        if ($document->getOwner() === $user) {
            $vote?->addReason('User is the document owner.');
            return true;
        }

        $vote?->addReason('User is neither the owner nor the document public.');
        return false;
    }
}

Essas razões aparecem no Symfony Profiler sob o painel de Security, mostrando exatamente por que cada decisão foi tomada. Isso elimina suposições ao depurar templates que exibem condicionalmente botões de edição ou exclusão.

Perguntas de entrevista técnica frequentes sobre segurança no Symfony

Estas perguntas aparecem com frequência em entrevistas para vagas de desenvolvimento Symfony, cobrindo desde conceitos fundamentais até decisões de arquitetura.

Qual é a diferença entre autenticação e autorização no Symfony?

A autenticação verifica a identidade (gerenciada por firewalls e authenticators). A autorização verifica as permissões (gerenciada por voters, roles e regras de access control). Um usuário pode estar autenticado mas não autorizado a realizar uma ação específica. O Symfony impõe essa separação de forma arquitetural: o firewall estabelece quem é o usuário, e a camada de autorização determina o que ele pode fazer.

Quando utilizar um voter em vez de uma verificação de role?

As verificações de roles (ROLE_ADMIN, ROLE_EDITOR) funcionam para permissões estáticas no nível do usuário. Os voters lidam com autorização dinâmica e dependente de contexto: "Este usuário pode editar este post específico?" A resposta depende do autor do post, do status de publicação ou da pertinência a uma equipe — informações que um role não codifica. A regra geral: se a decisão requer inspecionar o sujeito, deve-se usar um voter.

Como o CacheableVoterInterface melhora o desempenho?

Cada chamada a isGranted() invoca todos os voters registrados. Em uma aplicação com 40 voters chamados 500 vezes por requisição, isso gera uma sobrecarga significativa. O CacheableVoterInterface adiciona um método supportsAttribute() que permite ao Symfony pular os voters que não podem lidar com o atributo fornecido sem chamar supports() em cada voter. Os benchmarks mostram uma melhoria de 40% no desempenho do processamento de autorizações.

Como testar voters de forma isolada?

Os voters são classes PHP comuns sem dependências do framework além das interfaces. Testá-los requer criar um mock de TokenInterface com um usuário e chamar vote() diretamente:

tests/Security/Voter/PostVoterTest.phpphp
namespace App\Tests\Security\Voter;

use App\Entity\Post;
use App\Entity\User;
use App\Security\Voter\PostVoter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class PostVoterTest extends TestCase
{
    private PostVoter $voter;

    protected function setUp(): void
    {
        $this->voter = new PostVoter();
    }

    public function testAuthorCanEdit(): void
    {
        $user = new User();
        $post = (new Post())->setAuthor($user);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, $post, [PostVoter::EDIT]);

        $this->assertSame(VoterInterface::ACCESS_GRANTED, $result);
    }

    public function testNonAuthorCannotEdit(): void
    {
        $author = new User();
        $otherUser = new User();
        $post = (new Post())->setAuthor($author);
        $token = new UsernamePasswordToken($otherUser, 'main', $otherUser->getRoles());

        $result = $this->voter->vote($token, $post, [PostVoter::EDIT]);

        $this->assertSame(VoterInterface::ACCESS_DENIED, $result);
    }
}

Sem inicialização do kernel, sem banco de dados, sem requisição HTTP. Os voters são um dos componentes mais testáveis do stack de segurança do Symfony.

O que acontece quando security: false é definido em um firewall?

O firewall é completamente desativado para as rotas correspondentes. Nenhuma autenticação ocorre, nenhum usuário é carregado da sessão e nenhum evento de segurança é disparado. Isso é apropriado para assets estáticos e ferramentas de desenvolvimento (profiler, web debug toolbar), mas perigoso se aplicado a rotas que servem dados de usuário. Na documentação de segurança do Symfony, o firewall dev utiliza esse padrão exclusivamente para caminhos não sensíveis.

Não usar security: false em rotas de API

Definir security: false em um firewall que corresponde a rotas de API remove toda a autenticação. Diferentemente de uma regra de access control que retorna 401/403, um firewall desativado não fornece nenhum contexto de usuário. O registro de logs, a auditoria e a limitação de taxa baseados no usuário autenticado param completamente de funcionar.

Fortalecendo a segurança do Symfony além da configuração padrão

A configuração de segurança padrão do Symfony é razoável, mas não está preparada para produção. Três áreas merecem atenção:

A limitação de tentativas de login restringe as falhas por combinação IP+nome de usuário. A opção login_throttling na configuração do firewall (mostrada anteriormente) limita as tentativas a 5 por janela de 15 minutos. Sem isso, ataques de força bruta não encontram resistência.

A proteção CSRF deve estar sempre ativada no formulário de login (enable_csrf: true). O Symfony 7.4 também introduziu um SameOriginCsrfTokenManager que utiliza o header do navegador Sec-Fetch-Site para verificação adicional sem exigir gerenciamento de tokens em configurações com reverse proxy.

Os UserCheckers personalizados são executados após o sucesso da autenticação, mas antes que o usuário obtenha acesso. Eles validam condições como suspensão de conta, verificação de email ou status de assinatura:

src/Security/UserEnabledChecker.phpphp
namespace App\Security;

use App\Entity\User;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class UserEnabledChecker implements UserCheckerInterface
{
    public function checkPreAuth(UserInterface $user): void
    {
        if (!$user instanceof User) {
            return;
        }

        if ($user->isBanned()) {
            throw new CustomUserMessageAccountStatusException(
                'This account has been suspended.'
            );
        }
    }

    public function checkPostAuth(UserInterface $user): void
    {
        if (!$user instanceof User) {
            return;
        }

        if (!$user->isEmailVerified()) {
            throw new CustomUserMessageAccountStatusException(
                'Please verify your email address before logging in.'
            );
        }
    }
}

O registro na configuração do firewall é feito com user_checker: App\Security\UserEnabledChecker. O Symfony detecta automaticamente as classes que implementam UserCheckerInterface, mas o vínculo com o firewall deve ser explícito.

Eventos de segurança para auditoria

O Symfony despacha LoginSuccessEvent, LoginFailureEvent e LogoutEvent durante o ciclo de vida da autenticação. A assinatura desses eventos permite construir logs de auditoria, disparar alertas em padrões de login suspeitos ou sincronizar dados de sessão com sistemas de monitoramento externos.

Pronto para mandar bem nas entrevistas de Symfony?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Conclusão

  • Os firewalls definem as fronteiras de autenticação. É necessário separar os firewalls de API stateless dos firewalls web stateful e sempre definir lazy: true em firewalls baseados em sessão para adiar o carregamento do usuário
  • Os voters encapsulam as regras de autorização de negócio. Devem ser usados para qualquer verificação de permissão que dependa do sujeito (a entidade acessada), não apenas do role do usuário
  • O parâmetro ?Vote $vote (Symfony 7.3+) adiciona explicações às decisões dos voters, tornando a depuração de autorizações transparente no profiler e nos logs
  • O atributo #[IsGranted] mantém os controllers limpos ao declarar os requisitos de autorização de forma declarativa, com resolução automática do sujeito a partir dos argumentos do controller
  • As estratégias de decisão de acesso (affirmative, consensus, unanimous, priority) controlam como respostas conflitantes dos voters são resolvidas. affirmative por padrão é adequado para a maioria dos casos; unanimous é recomendado para rotas de alta segurança
  • O fortalecimento para produção requer limitação de tentativas de login, proteção CSRF e UserCheckers personalizados para validação de contas específica do negócio
  • Os voters são completamente testáveis unitariamente sem inicializar o kernel do Symfony, o que os torna um dos componentes mais limpos da arquitetura de segurança a manter

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#symfony
#security
#php
#voters
#firewalls
#authentication
#interview

Compartilhar

Artigos relacionados