Spring Security
Atualizar para o Spring Boot 3 e descobrir que o WebSecurityConfigurerAdapter foi descontinuado causou calafrios em muita gente. Além da mudança de sintaxe, há um erro arquitetural silencioso: muitas APIs usam JWT, mas continuam guardando estado na memória do servidor sem o desenvolvedor perceber. Neste post, vamos configurar a segurança da forma moderna, garantir que a sua API seja 100% Stateless e entender por que o Refresh Token é a peça chave para não arruinar a experiência do seu usuário.
O WebSecurityConfigurerAdapter
Se você trabalha com Java há alguns anos, já estava acostumado a criar uma classe de configuração, estender o WebSecurityConfigurerAdapter e sobrescrever o método configure(HttpSecurity http). Era o padrão do mercado.
Com a chegada do Spring Boot 3 (e Spring Security 6), essa classe foi completamente removida. O framework mudou para uma abordagem baseada puramente em Componentes (Beans) e adotou a sintaxe de Lambda DSL.
A intenção do Spring foi ótima: deixar o código mais limpo e desacoplado. Mas, na prática, quem tentou atualizar projetos legados teve muitos erros para corrigir.
Veja como fica a configuração moderna e enxuta, instanciando um SecurityFilterChain:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // Desabilitamos o CSRF pois usaremos JWT
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // Rotas públicas (Login/Cadastro)
.anyRequest().authenticated() // O resto exige token
)
// A mágica do Stateless acontece aqui:
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Inserimos nosso filtro customizado ANTES do filtro padrão do Spring
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
JWT "Stateless"
Muitos desenvolvedores geram o token JWT na rota de login, enviam para o Front-end e criam um filtro para validar esse token nas próximas requisições. O problema é que eles esquecem de configurar a política de sessão, ou seja, ignoram a linha .sessionManagement(...) do código acima.
O que acontece quando você esquece isso? O Spring Security, por padrão, assume que você está em uma aplicação web tradicional e cria uma sessão em memória, gerando um cookie chamado JSESSIONID.
Você acha que a sua API é Stateless porque está usando JWT, mas o seu servidor está secretamente alocando memória para cada usuário que faz login. Se a sua aplicação escalar e receber um pico de acessos, a memória RAM do servidor vai chegar ao limite, e você terá problemas para balancear a carga entre múltiplas instâncias (já que a sessão ficou presa na máquina A e não existe na máquina B).
Ao forçar o SessionCreationPolicy.STATELESS, você diz ao Spring: "Esqueça o usuário assim que devolver a resposta HTTP. A única prova de identidade que aceitaremos na próxima requisição é um JWT válido."
O Filtro Customizado: Onde a identidade é validada
Em uma API puramente Stateless, o Spring não sabe quem é o usuário a menos que você conte a ele a cada requisição. Fazemos isso interceptando a chamada HTTP antes dela chegar ao Controller, através de um filtro (geralmente estendendo OncePerRequestFilter).
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
// Construtor omitido para brevidade...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
// Se não tem token, passa direto (o Spring vai bloquear lá na frente se a rota for protegida)
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUsername(jwt);
// Se o token tem um e-mail e o usuário ainda não está autenticado no contexto atual
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
// Autentica o usuário artificialmente APENAS para o ciclo de vida desta requisição
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Experiência do Usuário e o Refresh Token
O grande dilema arquitetural do JWT é o tempo de expiração. Se você colocar um token JWT para expirar em 1 ano, você cria uma falha de segurança absurda. Se o token for roubado, o invasor terá acesso irrestrito, pois um JWT Stateless não pode ser revogado facilmente no servidor.
Por outro lado, se você colocar o token para expirar em 15 minutos, a segurança fica excelente. Mas imagine a frustração: um usuário está focado no seu trabalho e de repente toma um logout porque o token expirou. É uma experiência terrível que destrói o engajamento de qualquer sistema.
A solução definitiva: Access Token + Refresh Token
1. Access Token: É o JWT normal, com vida curta (ex: 15 minutos). É usado em todas as requisições para a API.
2. Refresh Token: É uma string aleatória (geralmente salva no banco de dados e associada ao usuário), com vida longa (ex: 7 dias ou 30 dias). Ele é enviado para o Front-end no momento do login, normalmente dentro de um cookie HttpOnly para evitar roubos via JavaScript.
O fluxo: Quando o Access Token de 15 minutos expira, a API retorna um Erro 401 Unauthorized. O Front-end, de forma invisível para o usuário, pega o Refresh Token guardado, bate em uma rota específica (/api/auth/refresh), e troca esse token longo por um novo Access Token fresquinho de 15 minutos.
O usuário não percebe absolutamente nada, a segurança do sistema é mantida em alto nível, e o engajamento da plataforma fica protegido contra quedas de sessão.
Conclusão
Atualizar o Spring Security pode assustar no começo, mas a nova sintaxe orientada a componentes trouxe um código muito mais declarativo e elegante. O segredo de uma API REST robusta não está apenas em decorar as anotações do framework, mas em compreender os fundamentos do protocolo HTTP e os trade-offs entre segurança (Tokens curtos) e usabilidade (Refresh Tokens).