Filtro JWT personalizado en Spring Security

Resumen

Crear un filtro JWT propio en Spring Security te permite interceptar cada petición que llega a tu API, validar el token y cargar al usuario autenticado en el contexto de seguridad. Esto resulta clave si trabajas con autenticación bearer token y necesitas integrar tu lógica con la cadena de filtros de Spring.

¿Qué hace un filtro JWT dentro del Spring Security Filter Chain?

Un filtro personalizado captura cada petición entrante y decide si la información de autorización es válida antes de dejarla pasar al resto de la cadena. Para eso creas una clase dentro del paquete config, la anotas con @Component para que Spring la registre como bean, y la haces extender de OncePerRequestFilter. Esa herencia te obliga a implementar el método doFilterInternal, que recibe HttpServletRequest, HttpServletResponse y FilterChain [00:51].

¿Por qué extender de OncePerRequestFilter? Porque garantiza que el filtro se ejecute una sola vez por petición, evitando validaciones duplicadas dentro de la misma cadena de seguridad.

Tu filtro tendrá cuatro pasos claros:

  • Validar que el header Authorization sea válido.
  • Validar que el JSON Web Token también lo sea.
  • Cargar el usuario desde el UserDetailsService.
  • Cargar al usuario en el contexto de seguridad de Spring.

¿Cómo validar el header Authorization en Java?

El primer paso es obtener el encabezado con request.getHeader(HttpHeaders.AUTHORIZATION). Esa constante de org.springframework.http representa la palabra Authorization sin que tengas que escribirla manualmente [03:35].

Después viene la triple validación. Si el authHeader es null, está vacío o no empieza por la palabra Bearer, la petición no es válida en términos de seguridad. En cualquiera de esos casos, le dices al filter chain que continúe con filterChain.doFilter(request, response) y haces return para cortar la ejecución [05:40].

¿Por qué el header debe iniciar con Bearer?

Cuando usas autenticación con bearer token en herramientas como Postman, el encabezado Authorization se construye con la palabra Bearer, un espacio en blanco y luego el JSON Web Token. Si ese prefijo no aparece, la petición no cumple el formato esperado y no tiene sentido seguir procesándola.

¿Cómo extraer y validar el JSON Web Token?

Una vez que el encabezado pasa las validaciones, toca capturar el token. Usas authHeader.split(" ") con un espacio como regex, lo que crea un arreglo donde la posición cero contiene Bearer y la posición uno tu JWT. Aplicas .trim() para limpiar espacios sobrantes [07:20].

Luego inyectas tu JWTUtil mediante @Autowired en el constructor y llamas a this.jwtUtil.isValid(jsonWebToken). Si el resultado es falso, vuelves a delegar al filter chain y haces return. Esa negación deja el código más limpio que comparar contra false explícitamente.

¿Qué pasa si no pongo return después de filterChain.doFilter? Aunque el método sea void, sin el return el código sigue ejecutándose y podría intentar cargar un usuario inválido en el contexto, rompiendo la seguridad de la aplicación.

¿Cómo cargar al usuario y autenticarlo en el contexto de Spring?

Con el token validado, obtienes el username usando jwtUtil.getUsername(jsonWebToken). No necesitas un try-catch aquí porque la validación previa ya garantizó que el token es correcto [10:15].

Luego inyectas la interfaz UserDetailsService. Gracias al polimorfismo y a los beans de Spring, no hace falta especificar la implementación concreta: como tu UserSecurityService es el único que implementa esa interfaz dentro del proyecto, Spring lo resuelve automáticamente. Llamas a userDetailsService.loadUserByUsername(username) y casteas el resultado a tu clase User.

¿Qué constructor de UsernamePasswordAuthenticationToken debo usar?

Existen dos opciones y la diferencia es enorme:

  • Constructor de dos parámetros: recibe usuario y contraseña, marca la petición como no autenticada y se usa en el flujo de login con AuthenticationManager.
  • Constructor de tres parámetros: recibe usuario, contraseña y granted authorities, y marca la petición como autenticada desde el inicio.

En tu filtro JWT necesitas el constructor de tres parámetros, porque la autenticación ya ocurrió cuando se generó el token al iniciar sesión. Lo construyes así: new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities()) [13:45].

Finalmente, cargas la autenticación al contexto con SecurityContextHolder.getContext().setAuthentication(authenticationToken). Esa línea es donde ocurre la magia: le indica al resto de filtros de la cadena que esta petición está resuelta correctamente en términos de seguridad. Cierras el método con filterChain.doFilter(request, response) para que continúe el flujo normal.

¿Qué diferencia hay entre los distintos doFilter dentro del filtro?

Las llamadas a filterChain.doFilter que aparecen cuando algo es inválido no cargan nada en el contexto de seguridad, lo que termina generando una respuesta 401 o 403. En cambio, la última llamada se ejecuta después de cargar la autenticación, garantizando que la petición avance con un usuario válido.

Con esto ya tienes tu filtro listo para integrarse al Spring Security Filter Chain. ¿Te animas a probarlo con tu propio endpoint protegido y compartir cómo te fue en los comentarios?