Spring Framework
Fundamentos do Spring Security: filter chain, autenticação e autorização
Intermediário8 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O Spring Security é uma cadeia de filtros servlet configurada por um bean SecurityFilterChain. Ao final deste guia, você vai entender como uma requisição é autenticada e autorizada, como declarar usuários e codificar senhas do jeito certo, como proteger URLs e métodos com a DSL lambda e @PreAuthorize, o que a proteção contra CSRF faz e quando desativá-la, e como o mesmo modelo se estende a APIs baseadas em token.
O que você vai aprender
- Explicar o papel do DelegatingFilterProxy, do FilterChainProxy e de cada filtro de segurança
- Configurar uma SecurityFilterChain com a DSL lambda para form login, HTTP Basic e APIs stateless
- Fornecer usuários via UserDetailsService e armazenar senhas com um DelegatingPasswordEncoder
- Autorizar requisições por padrão de URL e métodos com @PreAuthorize, diferenciando roles de authorities
- Descrever contra o que a proteção CSRF protege e decidir quando ela pode ser desativada
Segurança é o aspecto que a maioria das aplicações erra, não porque seja difícil de adicionar, mas porque é difícil adicionar em todo lugar. O Spring Security resolve isso se colocando na frente de cada requisição como uma cadeia de filtros servlet e na frente dos métodos escolhidos como um interceptor AOP, de modo que autenticação e autorização são declaradas uma vez e aplicadas de forma consistente. Este guia cobre o Spring Security 6.x usado com o Spring Framework 6 no Java 17 ou superior, com o modelo de configuração Java que substituiu o WebSecurityConfigurerAdapter; OAuth 2.0 e os formatos de token em si são tratados na trilha de Segurança.
Arquitetura: a filter chain
O servlet container conhece um único filtro: o DelegatingFilterProxy, registrado com o nome springSecurityFilterChain. Ele existe para que um filtro gerenciado pelo container possa delegar para um bean do Spring. Esse bean é o FilterChainProxy, que guarda uma ou mais instâncias de SecurityFilterChain, cada uma associada a um padrão de requisição. Para cada requisição, a primeira cadeia que casa executa sua lista ordenada de filtros — cerca de quinze numa configuração típica com form login. Os que vale a pena conhecer:
| Filtro | Responsabilidade |
|---|---|
SecurityContextHolderFilter | Carrega o SecurityContext (da sessão, por padrão) no SecurityContextHolder durante a requisição e o limpa ao final |
CsrfFilter | Rejeita requisições que alteram estado sem um token CSRF válido |
UsernamePasswordAuthenticationFilter | Trata o POST do formulário de login |
BasicAuthenticationFilter | Trata o header Authorization: Basic |
BearerTokenAuthenticationFilter | Trata Authorization: Bearer em resource servers |
ExceptionTranslationFilter | Transforma uma AuthenticationException num desafio (redirecionamento para o login, 401) e uma AccessDeniedException em 403 |
AuthorizationFilter | Aplica as regras de authorizeHttpRequests — o último filtro antes da sua aplicação |
A autenticação é uma conversa em duas etapas. Um filtro monta, a partir da requisição, um token Authentication ainda não autenticado — usuário e senha, um bearer token — e o entrega a um AuthenticationManager. A implementação padrão, ProviderManager, tenta cada AuthenticationProvider até que um deles suporte o token; o DaoAuthenticationProvider carrega o usuário por meio de um UserDetailsService, confere a senha com um PasswordEncoder e retorna uma Authentication completa, com a lista de GrantedAuthority do usuário. O filtro a guarda no SecurityContextHolder e, em aplicações com sessão, a persiste na HttpSession para que as requisições seguintes não precisem passar de novo pelo login.
A autorização acontece depois, e duas vezes: o AuthorizationFilter verifica as regras de URL e, se a segurança de métodos estiver habilitada, um interceptor AOP verifica o @PreAuthorize do método chamado. Os dois consultam um AuthorizationManager com a Authentication atual.
Configurando uma SecurityFilterChain
Desde o Spring Security 5.7, a configuração é um bean, não uma subclasse. Com Spring Boot, basta ter o spring-boot-starter-security para o filtro ser registrado; num WAR de Spring MVC puro, você ainda precisa declarar uma classe que estenda AbstractSecurityWebApplicationInitializer para que o container reconheça o DelegatingFilterProxy.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/about", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/orders/**").hasAuthority("orders:write")
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login") // your own GET /login view; POST /login is handled by the filter
.permitAll())
.logout(logout -> logout.logoutSuccessUrl("/"))
.httpBasic(Customizer.withDefaults()); // also accept Basic, for scripts
return http.build();
}
@Bean
UserDetailsService users(PasswordEncoder encoder) {
var alice = User.withUsername("alice")
.password(encoder.encode("correct horse battery staple"))
.roles("USER", "ADMIN") // stored as ROLE_USER, ROLE_ADMIN
.build();
var bob = User.withUsername("bob")
.password(encoder.encode("bob-password-2026"))
.authorities("ROLE_USER", "orders:write") // fine-grained: a role plus a permission
.build();
return new InMemoryUserDetailsManager(alice, bob);
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
O estilo lambda é a única DSL no Spring Security 7; a antiga forma encadeada com .and() foi marcada como deprecated na 6.1, então escreva a forma lambda desde já. As regras de authorizeHttpRequests são avaliadas na ordem de declaração e a primeira que casa vence, por isso os padrões específicos precisam vir antes de anyRequest(). permitAll ainda faz a requisição passar pela cadeia — o usuário é identificado se houver uma sessão —, enquanto web.ignoring() pula a segurança por completo e deve ficar reservado para recursos realmente estáticos.
hasRole("ADMIN") verifica a authority ROLE_ADMIN; hasAuthority("orders:write") verifica a string literal. Roles são grupos de granularidade grossa, authorities são permissões individuais; sistemas grandes costumam armazenar permissões e derivá-las das roles no login.
Usuários e senhas
O gerenciador em memória acima serve para demos e testes. Aplicações em produção implementam UserDetailsService sobre o próprio armazenamento:
@Service
public class AccountUserDetailsService implements UserDetailsService {
private final AccountRepository accounts;
public AccountUserDetailsService(AccountRepository accounts) { this.accounts = accounts; }
@Override
public UserDetails loadUserByUsername(String username) {
Account a = accounts.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username)); // message is not shown to the client
return User.withUsername(a.email())
.password(a.passwordHash()) // already encoded, e.g. "{bcrypt}$2a$10$…"
.authorities(a.permissions().stream().map(SimpleGrantedAuthority::new).toList())
.accountLocked(a.locked())
.disabled(!a.active())
.build();
}
}
Para schemas padrão existem o JdbcUserDetailsManager e uma variante LDAP. Seja qual for o armazenamento, as senhas precisam passar por hash com um algoritmo lento e com salt. PasswordEncoderFactories.createDelegatingPasswordEncoder() retorna um DelegatingPasswordEncoder que codifica com bcrypt por padrão e prefixa cada hash com um identificador — {bcrypt}, {argon2}, {pbkdf2} — para que o algoritmo possa ser trocado mais tarde sem deixar de verificar os hashes antigos. Nunca armazene texto puro; {noop} existe só para testes. Como a verificação é lenta de propósito (bcrypt na força padrão leva dezenas de milissegundos ou mais por checagem), autentique uma vez por sessão em vez de a cada requisição, que é exatamente o que a segurança baseada em sessão faz.
Segurança de métodos
Regras de URL protegem os pontos de entrada; a segurança de métodos protege as operações por trás deles, inclusive as alcançadas por outros caminhos — um job agendado, um listener de mensagens, um segundo controller. @EnableMethodSecurity (que substituiu @EnableGlobalMethodSecurity na 6.0) habilita @PreAuthorize, @PostAuthorize, @PreFilter e @PostFilter com a Spring Expression Language:
@Service
public class OrderService {
@PreAuthorize("hasAuthority('orders:write')")
public Order place(CreateOrder cmd) { … }
@PreAuthorize("hasRole('ADMIN') or #customerId == authentication.name")
public List<Order> ordersOf(String customerId) { … }
@PostAuthorize("returnObject.customerId == authentication.name or hasRole('SUPPORT')")
public Order get(long id) { … }
}
Como isso é AOP, valem as regras de sempre dos proxies: a checagem só roda quando a chamada chega pelo proxy, não em autoinvocação, e uma chamada negada lança AccessDeniedException — que, se escapar até a camada web, o ExceptionTranslationFilter transforma num 403. Coloque as anotações na camada de service, onde a operação de negócio é definida, e não em controllers que só repassam a chamada.
Proteção contra CSRF
Um ataque de cross-site request forgery engana o navegador de um usuário logado para que ele envie uma requisição que altera estado — um POST de formulário para o seu site a partir de uma página controlada pelo atacante —, e o navegador anexa o cookie de sessão automaticamente. O Spring Security se defende exigindo que toda requisição não segura (POST, PUT, PATCH, DELETE) carregue um token que a página do atacante não consegue ler: o CsrfFilter compara o token da requisição (um campo de formulário chamado _csrf ou o header X-CSRF-TOKEN) com o que está guardado no servidor. A integração do Thymeleaf com o Spring e a tag de formulário do JSP adicionam o campo oculto automaticamente; um cliente JavaScript o lê do cookie XSRF-TOKEN quando você configura CookieCsrfTokenRepository.withHttpOnlyFalse().
O Spring Security 6 mudou dois padrões: o token é carregado de forma lazy (só quando uma requisição realmente precisa dele) e o valor exposto à requisição é mascarado a cada requisição pelo XorCsrfTokenRequestAttributeHandler, o novo handler padrão, para resistir a ataques do tipo BREACH. As duas mudanças são transparentes para aplicações baseadas em formulário e um pouco mais trabalhosas para single-page apps que leem o token de um cookie; a seção sobre SPA do guia de referência mostra a configuração exata.
A proteção só faz sentido quando o navegador se autentica automaticamente. Uma API autenticada por um bearer token num header é imune, e é por isso que a configuração stateless abaixo a desativa.
APIs stateless
Um resource server que valida bearer tokens a cada requisição não precisa de sessão, nem de página de login, nem de CSRF:
@Bean
@Order(1)
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // this chain only for the API
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())) // validates JWTs against the issuer's JWKS
.exceptionHandling(e -> e.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()));
return http.build();
}
Várias cadeias são ordenadas com @Order; a primeira cujo securityMatcher casa trata a requisição, então a cadeia da API precisa vir antes de uma cadeia web genérica que pega tudo. oauth2ResourceServer().jwt() exige os módulos spring-security-oauth2-resource-server e spring-security-oauth2-jose, além de um issuer ou de uma JWK Set URI; os scopes do token viram authorities com o prefixo SCOPE_, que você pode remapear com um JwtAuthenticationConverter. Decodificar um token para ver as claims durante a depuração é tarefa para o decodificador de JWT — nunca para confiar nele; só a verificação de assinatura do resource server faz isso.
Acessando o usuário atual
@GetMapping("/me")
public ProfileResponse me(@AuthenticationPrincipal UserDetails user) {
return profiles.forUsername(user.getUsername());
}
// in a service, without web types
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
boolean admin = auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"));
O contexto fica guardado num ThreadLocal durante toda a requisição. Trabalho repassado para outra thread — @Async, um executor, uma parallel stream — não o enxerga, a menos que você envolva o executor com DelegatingSecurityContextExecutor ou mude a estratégia do holder para MODE_INHERITABLETHREADLOCAL.
Erros comuns
Colocar anyRequest() primeiro. Todas as regras seguintes ficam inalcançáveis; o Spring Security 6 lança exceção na inicialização para regras declaradas depois de anyRequest(), mas um padrão amplo /** colocado cedo demais encobre silenciosamente os específicos.
Prefixar hasRole com ROLE_. hasRole("ROLE_ADMIN") lança IllegalArgumentException. Use hasAuthority se quiser a string literal.
Codificar senhas no login em vez de no cadastro. A verificação usa PasswordEncoder.matches(raw, encoded); nunca compare encode(raw) com o hash armazenado — algoritmos com salt produzem uma saída diferente a cada vez.
Desativar CSRF numa aplicação com cookie de sessão "porque estava atrapalhando". Você acabou de liberar justamente o ataque que o filtro impede. Corrija a integração do formulário ou da SPA.
Revelar se um nome de usuário existe. Por padrão, UsernameNotFoundException é convertida num erro genérico de credenciais inválidas (hideUserNotFoundExceptions); não desfaça isso, e mantenha na página de login a mesma mensagem para usuário inexistente e para senha errada.
Confiar só nas regras de URL. Um novo mapping de controller fora dos padrões protegidos fica aberto por padrão, a menos que anyRequest().authenticated() feche a brecha; a segurança de métodos no service pega o que a camada de URL deixa passar.
Esquecer o CORS na cadeia de segurança. Requisições preflight OPTIONS passam pela filter chain antes de chegar ao MVC; chame http.cors(Customizer.withDefaults()) com um bean CorsConfigurationSource para que elas sejam permitidas.
Principais pontos
- O Spring Security é um
FilterChainProxyde filtros ordenados, registrado viaDelegatingFilterProxy; os filtros de autenticação preenchem oSecurityContextHoldere oAuthorizationFilteraplica as regras de URL. - A configuração é feita com beans
SecurityFilterChaine a DSL lambda; oWebSecurityConfigurerAdapterdeixou de existir na 6.0. - Os usuários vêm de um
UserDetailsService; as senhas são armazenadas com umDelegatingPasswordEncoder(bcrypt por padrão) e verificadas commatches. hasRoleadiciona o prefixoROLE_,hasAuthoritynão;@PreAuthorizenos services protege as operações seja qual for o ponto de entrada, respeitadas as regras dos proxies AOP.- A proteção CSRF importa sempre que o navegador se autentica automaticamente; APIs stateless com bearer token a desativam e usam
SessionCreationPolicy.STATELESS. - O contexto de segurança é vinculado à thread; propague-o explicitamente para o trabalho em segundo plano.
Fontes e leituras complementares
Perguntas frequentes
- O que aconteceu com o WebSecurityConfigurerAdapter?
- Ele foi marcado como deprecated no Spring Security 5.7 e removido na 6.0. Em vez de estender uma classe base, você declara um bean SecurityFilterChain que recebe um builder HttpSecurity, e um bean UserDetailsService em vez de sobrescrever configure(AuthenticationManagerBuilder). Várias cadeias podem coexistir, cada uma associada a um prefixo de URL com securityMatcher.
- Qual a diferença entre hasRole e hasAuthority?
- Os dois comparam com as strings GrantedAuthority da Authentication. hasRole('ADMIN') adiciona automaticamente o prefixo ROLE_, então verifica a authority ROLE_ADMIN; hasAuthority('ROLE_ADMIN') verifica a string literal. Use hasRole para roles de granularidade grossa e hasAuthority para permissões finas como orders:write. Nunca passe para hasRole uma string que já começa com ROLE_: isso lança uma exceção.
- Devo desativar a proteção CSRF numa API REST?
- Só se a API não for autenticada por nada que o navegador envie automaticamente. Se os clientes se autenticam com um bearer token no header Authorization, uma página de outro site não consegue forjar a requisição e a proteção CSRF não acrescenta nada: desative-a e defina a política de sessão como STATELESS. Se a API usa cookies de sessão — uma single-page app no mesmo site, por exemplo —, mantenha-a ativa e use o token repository baseado em cookie para que o JavaScript consiga ler o token.
- Como obtenho o usuário atual num controller ou num service?
- Num controller, declare um parâmetro @AuthenticationPrincipal UserDetails ou um java.security.Principal. Num service, SecurityContextHolder.getContext().getAuthentication() retorna a Authentication atual; o contexto é vinculado à thread da requisição pelo SecurityContextHolderFilter, então não fica disponível em outras threads a menos que você o propague de propósito.