Spring Boot 4 CSRF 403 em APIs REST - causa e correcao - Meu Universo Nerd

Você migrou pro Spring Boot 4, subiu em produção e de repente todo POST e PUT começou a responder 403 Forbidden. O GET funciona liso. O curl local passa. Os testes unitários estão verdes. Mas o front quebrou inteiro e ninguém mexeu numa linha de controller sequer. Você abre o log e não tem stack trace, não tem exceção de negócio, não tem nada. Só um 403 seco que aparece antes da requisição chegar no seu código.

Calma, não é maldição. A causa raiz não está no seu código: é o CSRF que agora vem ligado por padrão no Spring Security 7, que o Spring Boot 4 trouxe embutido. Nesse artigo a gente vai entender por que esse 403 aparece, qual a diferença entre o jeito errado e o jeito certo de resolver, e como ajustar sua configuração de segurança sem abrir um buraco que vai te assombrar na próxima auditoria. Bora?

Você migrou pro Spring Boot 4, subiu em produção e de repente todo POST e PUT começou a responder 403 Forbidden. O GET funciona liso. O curl local passa. Os testes unitários estão verdes. Mas o front quebrou inteiro e ninguém mexeu numa linha de controller sequer. Você abre o log e não tem stack trace, não tem exceção de negócio, não tem nada. Só um 403 seco que aparece antes da requisição chegar no seu código.

Calma, não é maldição. A causa raiz não está no seu código: é o CSRF que agora vem ligado por padrão no Spring Security 7, que o Spring Boot 4 trouxe embutido. Nesse artigo a gente vai entender por que esse 403 aparece, qual a diferença entre o jeito errado e o jeito certo de resolver, e como ajustar sua configuração de segurança sem abrir um buraco que vai te assombrar na próxima auditoria. Bora?

O que mudou no Spring Boot 4 (e por que isso te pegou de surpresa)

Fala galera. Antes de sair desligando coisa, bora entender o contexto.

O Spring Boot 4 subiu o Spring Security para a versão 7. E essa não foi uma atualização de número só. O Security 7 removeu de vez a classe WebSecurityConfigurerAdapter, que já estava deprecada desde o Security 5.7, e tornou obrigatório o estilo de configuração baseado em SecurityFilterChain com a DSL de lambdas. Junto com isso, vários comportamentos que antes você herdava "de graça" agora exigem decisão explícita.

O CSRF é o caso mais barulhento. Na verdade, a proteção CSRF sempre esteve ligada por padrão no Spring Security para requisições que mudam estado (POST, PUT, PATCH, DELETE). O que acontece é que muita gente, no Boot 2 e no começo do Boot 3, tinha configurações antigas que mascaravam isso, ou nem chegava a perceber porque o app usava sessão e formulário. Quando você migra pro Boot 4 e refatora a config de segurança para o novo formato, o comportamento padrão aparece sem filtro. Resultado: API REST stateless tomando 403 em toda escrita.

Segundo a documentação oficial do Spring Security sobre CSRF, o framework espera um token CSRF válido em qualquer requisição que não seja considerada segura (idempotente). Sem o token, ele barra antes de chegar no controller. É exatamente o que você está vendo.

Por que o 403 aparece: o CSRF explicado sem mistério

Pensa assim: CSRF (Cross-Site Request Forgery) é um ataque onde um site malicioso engana o navegador da vítima para fazer uma requisição autenticada no seu sistema, aproveitando que o cookie de sessão é enviado automaticamente. O token CSRF existe justamente pra impedir isso. O servidor gera um token, o front precisa devolver esse token num header, e quem não tiver o token toma 403.

Agora vem o detalhe que importa pra você: se sua API é stateless e usa JWT no header Authorization, o CSRF nem deveria se aplicar. Por quê? Porque o ataque CSRF depende de o navegador mandar credencial automaticamente (cookie). Token JWT no header não é enviado automático pelo navegador, então o vetor de ataque não existe. Só que o Spring Security não tem como adivinhar a sua arquitetura. Ele liga o CSRF por padrão e espera você dizer "essa API é stateless, pode desligar".

Olha um controller bem comum que para de funcionar depois da migração:

@RestController
@RequestMapping("/api/pedidos")
public class PedidoController {

    private final PedidoService pedidoService;

    // No Boot 3 isso funcionava. No Boot 4 retorna 403
    // antes mesmo de entrar no metodo.
    @PostMapping
    public ResponseEntity<Pedido> criar(@RequestBody PedidoRequest req) {
        Pedido pedido = pedidoService.criar(req);
        return ResponseEntity.status(HttpStatus.CREATED).body(pedido);
    }
}

Repara: o controller está perfeito. O problema não é ele. A requisição POST está sendo barrada lá atrás, no filtro de segurança, porque não veio com token CSRF. Como Tech Leader, peguei essa pegadinha de cheio numa migração de um serviço de pagamentos. A equipe passou meio dia caçando bug no controller e no front antes de cair a ficha de que o 403 vinha do filtro de CSRF. Por isso escrevi esse artigo, pra você não perder esse meio dia.

O jeito errado vs o jeito certo de resolver

O erro número 1 de quem migra é desligar o CSRF no grito e seguir a vida. Funciona, resolve o 403 na hora, mas se sua aplicação usa sessão e cookie em qualquer parte, você acabou de reabrir a porta pro ataque que o CSRF protegia. Bora ver os dois cenários direito.

Primeiro, como era no Boot 3 (com a classe que o Security 7 removeu):

// Spring Boot 3 / Security 6: compilava com @Deprecated
// No Boot 4 isso NEM COMPILA: a classe foi removida.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().authenticated()
            .and().httpBasic();
    }
}

Agora o jeito certo no Boot 4 para uma API REST stateless com JWT. Aqui sim a gente desliga o CSRF, mas de forma consciente e documentada, porque a arquitetura justifica:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // API stateless com JWT no header: o vetor CSRF nao
            // existe, entao desligamos de forma justificada.
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

Note duas mudanças que vão te pegar em todo projeto: authorizeRequests() virou authorizeHttpRequests(), e antMatchers() virou requestMatchers(). Quem mantém isso configurado vai bater nessas duas trocas com certeza. Se quiser se aprofundar na arquitetura stateless, eu detalhei o fluxo completo no guia de autenticação stateless com JWT no Spring Security.

Quando você NÃO deve desligar o CSRF (o caso SPA + cookie)

Aqui está a parte que separa o pleno do sênior. Se sua aplicação tem qualquer fluxo baseado em sessão e cookie, por exemplo um SPA (React, Angular) conversando com um backend Spring que usa cookie de sessão, desligar o CSRF é furo de segurança. O jeito certo é manter o CSRF ligado e expor o token via cookie, pro front ler e reenviar no header. O Spring Security 7 já tem suporte de primeira classe pra isso:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // App com sessao/cookie (SPA + backend): MANTEM o CSRF.
        // O token vai num cookie que o front le e devolve no header.
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated())
        .formLogin(Customizer.withDefaults());
    return http.build();
}

Com isso, o navegador recebe o cookie XSRF-TOKEN, o front lê esse valor e manda de volta no header X-XSRF-TOKEN em cada escrita. A proteção continua de pé e o 403 some, porque agora o token chega certinho. Esse é o padrão que a galera de segurança espera ver num code review sério.

Resumindo a decisão, que é o que o entrevistador sênior quer ouvir: API 100% stateless com token no header pode desligar o CSRF com consciência. App com sessão ou cookie precisa manter o CSRF e usar o repositório de cookie. Não existe "desliga tudo e segue", tá?

O que muda na prática no seu dia a dia

Bom, e o que isso significa na hora de migrar de verdade? Olha o lado a lado das duas eras de configuração:

Spring Boot 3 / Security 6 Spring Boot 4 / Security 7
WebSecurityConfigurerAdapter (deprecated) SecurityFilterChain como @Bean (obrigatório)
authorizeRequests() authorizeHttpRequests()
antMatchers() / mvcMatchers() requestMatchers()
CSRF muitas vezes mascarado por config antiga CSRF explícito: você decide ligar ou desligar
Java 17 mínimo Java 21 mínimo (Jakarta EE 11)

Na prática, o checklist de migração para a parte de segurança fica assim: troque a herança de WebSecurityConfigurerAdapter por um @Bean de SecurityFilterChain, troque authorizeRequests por authorizeHttpRequests, troque antMatchers por requestMatchers, e tome a decisão consciente sobre CSRF conforme sua arquitetura. Se você usa o plugin OpenRewrite na migração, ele resolve boa parte dessas trocas mecânicas automaticamente, mas a decisão sobre CSRF é sua e ninguém automatiza isso por você.

Um aviso de quem já apanhou: teste o fluxo de escrita logo no começo da migração, não no final. Suba um endpoint POST simples, bata nele com o front real (não só com curl, porque o curl não carrega cookie de navegador) e veja se o 403 aparece. Pega o problema cedo, quando ainda é barato consertar. Esse mesmo cuidado vale pra outras pegadinhas silenciosas do Boot 4, como a que cobri no guia de breaking changes do Spring Boot 4 e no caso do SSRF silencioso que também explode na migração.

FAQ: as dúvidas que sempre aparecem sobre CSRF no Boot 4

Posso só usar csrf.disable() e seguir em frente?

Pode, mas só se a sua API for de fato stateless, sem sessão e sem cookie de autenticação, usando token no header. Se tiver qualquer fluxo com cookie, desligar o CSRF abre vulnerabilidade. Nesse caso, use o CookieCsrfTokenRepository e mantenha a proteção ligada.

Por que o GET funciona e só o POST dá 403?

Porque o Spring Security só exige token CSRF em métodos que mudam estado: POST, PUT, PATCH e DELETE. GET, HEAD, OPTIONS e TRACE são considerados seguros e passam sem token. Por isso a leitura funciona e a escrita quebra.

Meu curl passa mas o front quebra. Por quê?

Se você desligou o CSRF, ambos passam. Se o CSRF está ligado com repositório de cookie, o curl normalmente não está carregando o cookie de sessão nem reenviando o header do token, então em alguns cenários ele se comporta diferente do navegador. Sempre valide com o cliente real do seu sistema.

O WebSecurityConfigurerAdapter ainda existe em algum lugar?

Não. Ele foi removido de vez no Spring Security 7. Se seu projeto ainda estende essa classe, ele não vai nem compilar no Boot 4. A migração para SecurityFilterChain é obrigatória, não opcional. A referência do Baeldung sobre a depreciação mostra o passo a passo da conversão.

Isso vale também para WebFlux (reativo)?

O conceito de CSRF e a decisão stateless valem igual, mas a API muda: no WebFlux você configura via ServerHttpSecurity e SecurityWebFilterChain. O raciocínio sobre quando desligar é o mesmo, só a classe de configuração é diferente.

E agora: os 3 passos pra resolver isso hoje

  • Identifique sua arquitetura primeiro: API stateless com JWT no header, ou app com sessão e cookie? Essa resposta decide tudo.
  • Para API stateless: desligue o CSRF de forma justificada e deixe um comentário no código explicando o porquê, pra próxima pessoa não achar que foi descuido.
  • Para app com cookie: mantenha o CSRF ligado com CookieCsrfTokenRepository e ajuste o front pra reenviar o token no header.

Você já tomou esse 403 fantasma na migração pro Spring Boot 4? Resolveu desligando ou manteve o CSRF com cookie? Conta nos comentários, quero saber como foi na sua stack. Pra quem ainda vai migrar, o relato de quem já passou economiza horas.

Na próxima semana vou mostrar o checklist completo de migração de segurança do Spring Boot 3 para o 4, com OpenRewrite configurado e os pontos que o plugin NÃO resolve sozinho. Assina o canal no YouTube pra não perder.