# 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. - Published: 2026-06-18 - Updated: 2026-06-18 - Author: SharpSkill - Tags: symfony, security, php, voters, firewalls, authentication, interview - Reading time: 10 min --- 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](https://symfony.com/doc/current/security.html#limiting-login-attempts). ## 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`: ```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; 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 `, 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: ```php // src/Security/Voter/PostVoter.php 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()`: ```php // src/Controller/PostController.php 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`: ```php // src/Security/Voter/DocumentVoter.php 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](https://symfony.com/blog/new-in-symfony-5-4-faster-security-voters) 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: ```php // tests/Security/Voter/PostVoterTest.php 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](https://symfony.com/doc/current/security.html), 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](https://symfony.com/blog/hardening-symfony-recent-security-improvements) 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: ```php // src/Security/UserEnabledChecker.php 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. ## 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/symfony/symfony-security-voters-firewalls-interview-questions