Implementando JWT con Spring Security 6

En Spring Security 6 desapareció WebSecurityConfigurerAdapter. Si venís de la versión 5, el cambio desorienta al principio, pero el nuevo modelo basado en beans es más claro una vez que lo entendés.

El flujo básico

Un cliente envía credenciales → el servidor valida y devuelve un JWT → el cliente lo incluye en el header Authorization: Bearer <token> en cada request → el servidor verifica la firma y extrae los claims.

Configuración del SecurityFilterChain

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
        this.jwtAuthFilter = jwtAuthFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

El filtro JWT

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain chain
    ) throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        final String token = authHeader.substring(7);
        final String username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            var userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtService.isValid(token, userDetails)) {
                var authToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
                );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        chain.doFilter(request, response);
    }
}

Consideraciones de seguridad

  • Usá tiempos de expiración cortos (15-30 min) y refresh tokens de larga duración en HttpOnly cookies
  • Nunca guardes el JWT en localStorage — es vulnerable a XSS
  • Firmá con algoritmos asimétricos (RS256) si el token lo van a validar terceros
  • Invalidación de tokens: JWT es stateless por naturaleza; si necesitás revocar sesiones, mantené una denylist en Redis