Hemos cogido carrerilla y ya vamos por la 12ª entrega de nuestra serie sobre patrones de arquitectura de microservicios. Como siempre, os animo a que echéis un vistazo a toda la serie de posts:

  1. Patrones de arquitectura de microservicios, ¿qué son y qué ventajas nos ofrecen?.
  2. Patrones de arquitectura: organización y estructura de microservicios.
  3. Patrones de arquitectura: comunicación y coordinación de microservicios.
  4. Patrones de arquitectura de microservicios: SAGA, API Gateway y Service Discovery.
  5. Patrones de arquitectura de microservicios: Event Sourcing y arquitectura orientada a eventos (EDA).
  6. Patrones de arquitectura de microservicios: comunicación y coordinación con CQRS, BFF y Outbox.
  7. Patrones de microservicios: escalabilidad y gestión de recursos con escalado automático.
  8. Patrones de arquitectura: de monolitos a microservicios.
  9. Patrones de arquitectura en microservicios: configuración externalizada.
  10. Patrones de arquitectura en microservicios: Consumer-Driven Contract Testing.
  11. Patrones de arquitectura en microservicios: seguridad.

Retomamos

En el post anterior, hacíamos la introducción a los patrones de seguridad y nos adentramos en Token-based Authentication (Autenticación basada en tokens) y OAuth (Open Authorization). En este post continuamos con JWT.

Nota: Los ejemplos de código son únicamente para entender los conceptos. Puede haber partes incompletas o con trozos demostrativos que no aplicarían a entornos reales.

JWT (JSON Web Tokens)

Descripción general y estructura

JSON Web Tokens (JWT) es un estándar (RFC 7519) para representar declaraciones (claims) de forma segura, usando JSON y firmas digitales. Un JWT típico consta de tres partes:

Estructura JWT.
  1. Header: indica el algoritmo de firma (ejemplo: HS256, RS256) y el tipo de token (JWT).

Header (Base64-url): {"alg":"HS256","typ":"JWT"}

  1. Payload: contiene las claims, que son los datos que describen al sujeto (sub), la expiración (exp), en un timestamp, el emisor (iss), roles, etc.

Payload (Base64-url): {"sub":"5","email":"usuario@demo.com","roles":["ROLE_USER"],"exp":1700515039}

  1. Signature o Firma: se genera al aplicar un algoritmo criptográfico con una clave secreta o un par de claves (privada/pública) al Header + Payload.

Firma (Base64-url): generada con HMAC-SHA256.

Una vez formado, un JWT puede verse así (simplificado):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiI1IiwiZW1haWwiOiJ1c3VhcmlvQGRlbW8uY29tIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImV4cCI6MTcwMDUxNTAzOX0
.
NLk__3FHxpb3ZzRdC0s5EZhWpTnzKnLxK4bEB-tkL28

Escenario en e-Commerce

Supongamos que nuestro e-commerce tiene un servicio de autenticación que, en vez de emitir tokens opacos y guardarlos en una base de datos, emite directamente JWT firmados.

El diagrama de secuencia:

  1. El cliente manda credenciales a POST /auth/login.
  2. El Auth Service valida credenciales y genera un JWT con el ID de usuario, email, roles y fecha de expiración.
  3. El cliente almacena ese JWT localmente (en un localStorage seguro, un cookie de tipo HttpOnly, etc.).
  4. Cada vez que accede a un microservicio (Carrito, Pedidos, Catálogo), pasa el JWT en la cabecera Authorization: Bearer .
  5. El microservicio, al recibirlo, valida la firma y la expiración, extrae los claims y decide si el usuario puede o no realizar la operación.
Diagrama de secuencia.

El diagrama de flujo:

Diagrama de flujo.

El proceso, es el mismo pero con algo más de detalle:

La gran ventaja: no necesitamos consultar al Auth Service para cada solicitud. La desventaja principal es que si queremos revocar un token antes de que expire, tenemos que implementar una estrategia adicional (lista negra, cambio de clave, intervalos de expiración muy cortos, etc.).

Implementación en Spring Boot

En este apartado vamos a ver cómo integrar JWT (JSON Web Tokens) en una aplicación Spring Boot paso a paso.

Dependencias

Para empezar, es fundamental agregar las dependencias correspondientes a Spring Security y la librería para manejar tokens JWT. Generalmente se incluyen:

Esto permite al proyecto tanto configurar la protección de endpoints con Spring Security como firmar y analizar (parsear) tokens JWT.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Clase para generar JWT (JwtTokenProvider)

En este punto, se crea una clase (por ejemplo, JwtTokenProvider) responsable de generar el token una vez que el usuario se haya autenticado correctamente.

Se suele usar un método generateToken(Authentication authentication) que:

El payload del token suele contener:

Esta clase también se encargará de:

@Service
public class JwtTokenProvider {

    private final String jwtSecret = "MiSuperSecreto";
    private final long jwtExpirationInMillis = 3600000; // 1 hora

    // Crear JWT basado en datos de usuario
    public String generateToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMillis);

        // Ejemplo: claims: subject, roles, email, etc.
        return Jwts.builder()
                .setSubject(userDetails.getUsername()) // Podría ser "email" o "userId"
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS256, jwtSecret)
                .compact();
    }

    // Validar JWT
    public boolean validateToken(String token) { 
    try { 
        Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token); 
        return true; 
    } catch (ExpiredJwtException e) { 
        System.out.println("El token ha expirado.");
    } catch (SignatureException e) { 
        System.out.println("Firma del token inválida.");
    } catch (MalformedJwtException e) { 
        System.out.println("Token mal formado.");
    }
    return false;
}

    // Obtener 'subject' (normalmente username o userId)
    public String getUsernameFromJWT(String token) {
        Claims claims = Jwts.parser().setSigningKey(jwtSecret)
                .parseClaimsJws(token).getBody();
        return claims.getSubject();
    }
}

Filtro de autenticación con JWT

Para cada petición HTTP, necesitamos interceptar la cabecera Authorization y, si existe un token JWT, validarlo. Esto se hace con un filtro que extiende de OncePerRequestFilter:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
                                    throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (tokenProvider.validateToken(token)) {
                String username = tokenProvider.getUsernameFromJWT(token);

                // Cargar más detalles del usuario, por ejemplo roles. 
                // Podríamos tenerlos en la BD o codificados en los claims del token.
                // Aquí asumimos un role de ejemplo:
                UserDetails userDetails = new User(
                        username,
                        "",
                        Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER”))
                );

                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

Configuración de seguridad

En SecurityConfig (extendiéndolo de WebSecurityConfigurerAdapter o usando las nuevas aproximaciones basadas en Beans), registramos el filtro y definimos qué endpoints estarán protegidos o abiertos:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/auth/login", "/auth/register").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

Controlador de autenticación

Finalmente, necesitamos un controlador para manejar el login, donde:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        // Autenticamos con el "AuthenticationManager" de Spring
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // Generamos el JWT
        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(Collections.singletonMap("token", jwt));
    }
}

JWT: pros y contras

Pros:

Contras:

Ejemplo de arquitectura híbrida (OAuth2 + JWT) en e-Commerce

En muchos casos, se combinan varios patrones. Por ejemplo:

Esto ofrece lo mejor de ambos mundos:

En un e-commerce, podríamos:

Este enfoque reduce las barreras a nuevos usuarios (por ejemplo, no necesitan crear una cuenta más, sino que usan su cuenta de Google) y mantiene la eficiencia en la validación de tokens.

Flujo de Login con arquitectura híbrida Oauth + JWT.
Flujo de Login con arquitectura híbrida Oauth + JWT.

Consideraciones avanzadas y buenas prácticas

Manejo de Refresh Tokens

Tanto en Token-based como en JWT, podemos implementar la lógica de Refresh Tokens para evitar que el usuario se reloguee constantemente. La idea es la siguiente:

Revocación de Tokens

Firmas asimétricas vs simétricas

En JWT se puede usar un secreto compartido (HMAC) o par de claves asimétricas (RSA/ECDSA).

Seguridad en almacenamiento de claves

Nunca debemos almacenar la clave secreta en texto plano dentro del repositorio de código. Usar un gestor de secretos (Vault, AWS Secrets Manager, etc.) o variables de entorno. En entornos productivos, la seguridad de la clave es crítica para evitar falsificación de tokens.

HTTPS / TLS

Siempre es fundamental cifrar el canal de comunicación. Utilizar TLS (HTTPS) tanto para interacciones externas como para la comunicación interna entre microservicios (si es viable). Sin un canal cifrado, un atacante podría robar los tokens (sea JWT u otros).

Logging y auditorías

En un e-commerce, es común requerir logs de auditoría para rastrear cambios sensibles (compras, cancelaciones, modificaciones de producto). Incorporar la información del usuario (ID, roles) en los logs facilita la trazabilidad y el cumplimiento de normas.

Integración con Gateway API

En muchos diseños de microservicios, se utiliza un API Gateway (por ejemplo, Spring Cloud Gateway o NGINX). El Gateway puede interceptar las peticiones y realizar la verificación de los tokens antes de enrutarlas a los microservicios internos.

Esto simplifica la lógica de seguridad, ya que no se necesita agregar los filtros de autenticación en cada microservicio, aunque estos podrían tener validaciones adicionales si lo requieren.

Comparativa final entre los patrones

Característica Token-based Auth OAuth2 JWT
Facilidad de implementación Relativamente sencilla Moderada-alta (varios flujos) Moderada (requiere firma y parseo de tokens)
Escenario típico Apps internas, proyectos pequeños/medianos Login con proveedores externos, delegación a un server Microservicios con validación local, escalabilidad
Necesidad de servicio central Opcional (si se valida token en un store) Sí (Authorization Server) Solo para emisión, validación puede ser descentralizada
Revocación Sencilla si se almacena token en DB/Redis A través del Authorization Server Requiere listas negras o expiraciones cortas
Tamaño del token Normalmente pequeño (UUID) Depende del proveedor (puede ser un token opaco) Puede crecer con los claims
Uso de claims Opcional (puede guardarse solo un ID) Sí, en algunos flujos (depende de ID Token) Fuerte, claims definidos en el payload

Conclusiones y recomendaciones

Siempre evalúa tu carga de trabajo, la arquitectura, la experiencia de usuario y la complejidad del sistema antes de decantarte por un patrón. En ocasiones, una mezcla de estos enfoques es la mejor solución.

En una aplicación compleja, donde se manejan transacciones y datos personales, es imprescindible diseñar con cuidado el flujo de acceso y la gestión de tokens.

La seguridad en una arquitectura de microservicios es un tema amplio, que abarca autenticación, autorización, integridad de datos y protección contra múltiples vectores de ataque.

Al final, la mejor elección depende de tu contexto de negocio, tu stack tecnológico y los requisitos de escalabilidad y seguridad. Muchos sistemas de producción combinan, por ejemplo, OAuth2 con emisión de JWT, y un gateway central que intercepta y verifica las solicitudes para simplificar la arquitectura del resto de microservicios.

En cualquier caso, lo fundamental es mantener siempre la seguridad como una prioridad desde las primeras fases de diseño, asegurando un enfoque sistemático y robusto que proteja la confidencialidad, integridad y disponibilidad de tus servicios y, sobre todo, de los datos de tus clientes.

No terminamos aquí

Espero que te haya gustado esta sección de seguridad, la cual da para mucho que hablar. En la siguiente entrega veremos patrones de tolerancia a fallos que, como su propio nombre indica, nos permite que un sistema siga funcionando aun cuando ocurran errores o caídas de alguno de sus componentes.

Referencias y lecturas recomendadas

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete