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:

FiltroResponsabilidade
SecurityContextHolderFilterCarrega o SecurityContext (da sessão, por padrão) no SecurityContextHolder durante a requisição e o limpa ao final
CsrfFilterRejeita requisições que alteram estado sem um token CSRF válido
UsernamePasswordAuthenticationFilterTrata o POST do formulário de login
BasicAuthenticationFilterTrata o header Authorization: Basic
BearerTokenAuthenticationFilterTrata Authorization: Bearer em resource servers
ExceptionTranslationFilterTransforma uma AuthenticationException num desafio (redirecionamento para o login, 401) e uma AccessDeniedException em 403
AuthorizationFilterAplica 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 FilterChainProxy de filtros ordenados, registrado via DelegatingFilterProxy; os filtros de autenticação preenchem o SecurityContextHolder e o AuthorizationFilter aplica as regras de URL.
  • A configuração é feita com beans SecurityFilterChain e a DSL lambda; o WebSecurityConfigurerAdapter deixou de existir na 6.0.
  • Os usuários vêm de um UserDetailsService; as senhas são armazenadas com um DelegatingPasswordEncoder (bcrypt por padrão) e verificadas com matches.
  • hasRole adiciona o prefixo ROLE_, hasAuthority não; @PreAuthorize nos 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.