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:

  • Inicio.
  • (1) El usuario manda sus credenciales al Auth Service.
  • (2) El Auth Service valida en su base de datos.
  • ¿Credenciales válidas?
  • Sí: (3) Genera Access Token (JWT) y Refresh Token.
  • No: Retorna un error (401/403) y termina.
  • (4) Retorna esos tokens al usuario.
  • (5) El usuario llama a un microservicio (ejemplo: Carrito) con el Access Token.
  • (6) El microservicio valida localmente la firma y expiración del JWT.
  • ¿Token vigente?
  • Sí → (7) Operación exitosa: se responde al usuario.
  • No → El token está caducado o inválido: el usuario procede con el Refresh Token ((R)), solicitando un nuevo Access Token al Auth Service.
  • Si el Refresh Token es válido, el usuario recibe un nuevo Access Token y reintenta la llamada al microservicio (se regresa a (5) en el diagrama).
  • Fin del proceso.

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:

  • spring-boot-starter-security, para la gestión de la seguridad en Spring.
  • io.jsonwebtoken:jjwt, una librería popular para generar y validar tokens JWT en Java.

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:

  • Extrae los datos del principal autenticado (por ejemplo, UserDetails con el nombre de usuario, roles, etc.).
  • Determina la fecha de emisión (issuedAt) y la fecha de expiración (expiration).
  • Aplica una firma digital con HMAC (usando una clave secreta compartida) o RSA/EC (con clave privada/pública).
  • Retorna un string en formato JWT que incluye el header, el payload (claims) y la firma.

El payload del token suele contener:

  • sub (subject): el identificador del usuario (userId o email).
  • roles: una lista de roles o privilegios.
  • exp: fecha de expiración para evitar que el token sea válido indefinidamente.

Esta clase también se encargará de:

  • Verificar la firma y la validez temporal del token con el método validateToken(String token)
  • Extraer información del payload (por ejemplo, el username) mediante el método getUsernameFromJWT(String token).
@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:

  • Se reciben las credenciales del usuario (username/contraseña).
  • Se autentica con el AuthenticationManager.
  • Si la autenticación es correcta, el tokenProvider genera un JWT.
  • Se retorna dicho token en la respuesta.
@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:

  • Escalable: cada microservicio valida el token localmente.
  • Bajo acoplamiento: no se requieren consultas a un servidor central o a una base de datos.
  • JSON es fácil de decodificar en cualquier plataforma o lenguaje.

Contras:

  • Revocación compleja: si un usuario es bloqueado, se debe invalidar el token de forma explícita (lista negra) o usar expiraciones cortas.
  • Los tokens pueden crecer en tamaño si se añaden muchos claims.
  • Manejo seguro de la clave de firma es crítico: si la clave se filtra, cualquiera puede firmar tokens válidos.

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

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

  • Un Authorization Server (por ejemplo, Keycloak) que se encarga de la autenticación OAuth2.
  • Cuando el usuario se autentica, se le entrega un Access Token que en realidad es un JWT firmado por Keycloak.
  • Cada microservicio valida localmente la firma del token.

Esto ofrece lo mejor de ambos mundos:

  • Simplicidad de validación local (gracias a JWT).
  • Capacidad de integrar proveedores externos y manejar usuarios, roles, consolas de administración, etc. (gracias a OAuth2).

En un e-commerce, podríamos:

  • Permitir “Login con Google” y “Login con Facebook” (OAuth2) a través de Keycloak.
  • Keycloak emite un JWT con roles y scopes.
  • Nuestros microservicios (Carrito, Pedidos, Catálogo) reciben el JWT y lo validan sin necesidad de contactar permanentemente con Keycloak.

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:

  • Generamos dos tokens:
    • Access Token (duración corta, por ejemplo, 15 minutos).
    • Refresh Token (duración más larga, por ejemplo, 7 días).
  • Cuando el Access Token expira, el cliente utiliza el Refresh Token para solicitar uno nuevo.
  • Podemos invalidar el Refresh Token cuando el usuario hace logout o cuando detectamos actividad sospechosa.
  • Esto reduce el riesgo de tener un Access Token con vigencia muy larga* que, en caso de robo, permitiría acceso prolongado.

Revocación de Tokens

  • En Token-based con almacenamiento (Redis/BD), revocar un token es tan simple como borrarlo del almacén.
  • En JWT, la revocación exige más trabajo. Algunas estrategias:
    • Lista negra: mantener un registro de tokens invalidos (ID token, fecha de expiración), pero esto implica consultar dicha lista.
    • Cambiar la clave de firma: esto revoca todos los tokens a la vez, útil si hay un incidente de seguridad.
    • Expiración muy corta + Refresh Tokens: reduce la ventana de ataque.

Firmas asimétricas vs simétricas

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

  • HMAC es más simple de configurar, pero requiere compartir la misma clave en todos los microservicios.
  • RSA/ECDSA permite que cada microservicio solo conozca la clave pública, mientras que la clave privada reside en el Auth Service.

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
  • Token-based Auth: ideal para proyectos con requerimientos simples, o cuando se quiere un control muy preciso en una base de datos central para revocar tokens.
  • OAuth2: fundamental si necesitas permitir “Login con Google” o si tu empresa ya tiene un Identity Provider. Útil cuando diferentes aplicaciones quieren usar single sign-on (SSO).
  • JWT: muy popular en arquitecturas de microservicios, evita consultas frecuentes a un servidor central, pero la revocación es más compleja.

Conclusiones y recomendaciones

  • Elegir el patrón adecuado depende de tus requisitos de usabilidad, integraciones externas, escalabilidad y revocación de credenciales.
  • Token-based Auth es fácil de empezar y de entender, pero puede necesitar un almacenamiento central para la revocación.
  • OAuth2 es el estándar para delegar autenticación en proveedores externos o para permitir que aplicaciones de terceros accedan a tus APIs con un conjunto de permisos. Es más complejo, pero sumamente poderoso en entornos empresariales.
  • JWT ofrece la ventaja de la descentralización de la verificación. Por ello, es muy usado en microservicios. No obstante, requiere un enfoque cuidadoso para su revocación y el manejo de claves.
  • En cualquier aplicación la seguridad no se limita a la autenticación y autorización. Debes aplicar prácticas como:
    • Cifrado en tránsito (HTTPS).
    • Cifrado en reposo (datos sensibles en la base de datos).
    • Monitoreo y logging de actividades (logs con información de usuario, IP, timestamps).
    • Mecanismos de protección contra ataques de fuerza bruta o inyección.

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.