Continuamos con nuestra serie sobre patrones de arquitectura de microservicios, y en esta ocasión hablaremos de seguridad. Si te has perdido alguno de los posts anteriores de esta serie, te animo a que les eches un vistazo 😉:

  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. Configuración externalizada.
  10. Consumer-Driven Contract Testing.

Introducción general a la seguridad en microservicios

En un entorno de microservicios, la funcionalidad de una aplicación grande se fragmenta en múltiples servicios independientes, cada uno con responsabilidades concretas.

Seguimos con el hilo conductor de todos los posts que venimos haciendo, el ejemplo de un e-commerce, donde podría ser:

Cada uno de estos servicios se comunicará con el resto (o con un gateway) para completar los flujos de negocio. Sin embargo, cuando se construye un sistema distribuido, surge el desafío de cómo asegurar que cada solicitud proviene de un cliente legítimo y que las operaciones que se desean realizar están debidamente autorizadas.

Tradicionalmente, cuando se construía un sistema monolítico, la seguridad se gestionaba habitualmente mediante:

En microservicios, esto se complica porque:

Importancia del dato

En cualquier aplicación la seguridad es crucial si (poniendo de ejemplo un ecommerce):

Por lo tanto, asegurar que las comunicaciones están correctamente autenticadas y que cada usuario solo acceda a sus recursos es fundamental.

Patrones de seguridad en microservicios

Para este artículo, nos centraremos en tres patrones que suelen usarse en la práctica.

  1. Token-based Authentication (autenticación basada en tokens).
  2. OAuth (Open Authorization).
  3. JWT (JSON Web Tokens).

Si bien existen otros enfoques (SAML, Basic Auth sobre TLS, Kerberos, etc.), estos tres se han vuelto especialmente populares en el contexto de arquitecturas modernas de microservicios y APIs REST.

Nota. Como veremos, vamos a dividir en 2 las entregas de estos patrones. En el primero veremos Token-based Authentication y Oauth, mientras que en el segundo, JWT y alguna comparativa más interesante.

Nota 2. 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.

1 Token-Based Authentication (Autenticación Basada en Tokens)

Descripción general

En la autenticación basada en tokens, un cliente (por ejemplo, una aplicación web en JavaScript, una app móvil, etc.) envía credenciales (usuario, contraseña) a un servicio de autenticación. Tras validar las credenciales, dicho servicio emite un token (comúnmente un string único, por ejemplo un UUID o un hash). Este token se utiliza en adelante para todas las peticiones posteriores: el cliente lo adjunta normalmente en la cabecera Authorization (generalmente Bearer <token> ) y, a su vez, el servicio receptor valida ese token para conceder o denegar el acceso.

Token-based authentication

Ventajas:

Desventajas:

Escenario en e-commerce

Supongamos que tenemos:

El flujo sería algo así:

  1. Login: el cliente envía usuario y contraseña al Auth Service a través de un endpoint como POST /auth/login.
  2. Generación de token
  1. Uso del token

El diagrama de flujo quedaría así:

Diagrama del flujo

El diagrama de secuencia:

Diagrama de secuencia

Implementación detallada en Spring Boot

Veamos un ejemplo de cómo se puede configurar esta autenticación en un proyecto Java con Spring Boot.

  1. Dependencias recomendadas

En tu pom.xml (asumiendo Maven), podrías tener:

<dependencies>
    <!-- Para seguridad básica en Spring -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Para gestionar datos en Redis, si usamos Redis para tokens -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>


    <!-- Otras dependencias necesarias para tu e-commerce -->
    <!-- ... -->
</dependencies>
  1. Configuración de Redis (opcional pero común)

Si optamos por almacenar tokens en Redis, configuramos en application.yml:

spring:
  redis:
    host: localhost
    port: 6379
  1. Clase de servicio para manejo de tokens

Este servicio centraliza la lógica de:

@Service
public class TokenService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // Duración de los tokens en segundos, por ejemplo 2 horas
    private final long TOKEN_EXPIRATION_SEC = 7200;

    public String generateTokenForUser(String username) {
        String token = UUID.randomUUID().toString();


        // Guardamos en Redis con expiración
        // Podríamos guardar un objeto con roles, nombre, email, etc.
        redisTemplate.opsForValue().set(token, username, TOKEN_EXPIRATION_SEC, TimeUnit.SECONDS);
        return token;
    }

    public boolean isValidToken(String token) {
        String username = (String) redisTemplate.opsForValue().get(token);
        return username != null; // El token existe y no ha expirado
    }

    public String getUsernameFromToken(String token) {
        return (String) redisTemplate.opsForValue().get(token);
    }

    public void revokeToken(String token) {
        // Eliminar token de Redis manualmente si se desea revocarlo
        redisTemplate.delete(token);
    }

   public void revokeAllTokensForUser(String username) {
       Set<String> keys = redisTemplate.keys("*"); // Buscar todas las claves
       if (keys != null) {
          for (String key : keys) {
              String storedUsername = (String) redisTemplate.opsForValue().get(key);
              if (username.equals(storedUsername)) {
                  redisTemplate.delete(key); // Elimina los tokens del usuario
              }
          }
      }
  }
}

Aquí, cada token se asocia con un username, pero en un sistema de e-commerce real incluiríamos más detalles (roles, ID del usuario, etc.). Cada vez que generamos un token, usamos un UUID y lo guardamos con un tiempo de expiración definido en Redis.

  1. Filtro de autenticación (TokenAuthenticationFilter)

El filtro se encarga de interceptar cada solicitud HTTP, extraer el token (si existe) y validar su legitimidad. Solo si el token es válido, se marca al usuario como autenticado en el contexto de Spring Security.

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
                                    throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        String token = null;

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
        }

        if (token != null && tokenService.isValidToken(token)) {
            String username = tokenService.getUsernameFromToken(token);


            // Se podría cargar más info del usuario para asignar roles
            UserDetails userDetails = new User(username, "",
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));

            // Spring Security requiere un objeto de Authentication para "loguear" al usuario
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

            // Se establece en el contexto de seguridad de Spring
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        // Continúa la cadena de filtros
        filterChain.doFilter(request, response);
    }
}
  1. Configuración de seguridad

La clase SecurityConfig utiliza la infraestructura de Spring Security para definir:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, 
    TokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
        return http
            .csrf(csrf -> csrf.ignoringRequestMatchers("/auth/login")) // Mejor práctica
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/login", "/public/**").permitAll() // Sección pública
                .anyRequest().authenticated()
            )
            .addFilterBefore(tokenAuthenticationFilter, 
                UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}
  1. Controlador de autenticación

Podríamos crear un controlador para manejar el login:

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

    @Autowired
    private TokenService tokenService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        // Validar credenciales: esto podría implicar consultar una base de datos
        // Suponemos un usuario "demo" con la contraseña "password" para el ejemplo.


        if ("demo".equals(loginRequest.getUsername()) 
                && "password".equals(loginRequest.getPassword())) {
            String token = tokenService.generateTokenForUser(loginRequest.getUsername());
            return ResponseEntity.ok(Collections.singletonMap("token", token));
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                                 .body("Credenciales inválidas");
        }
    }
}

class LoginRequest {
    private String username;
    private String password;
    // Getters y Setters
}
  1. Flujo del proceso
Diagrama de autenticación basado en tokens

El diagrama muestra dos fases principales de la de la autenticación basada en tokens:

Fase 1: flujo de login

Fase 2: llamada a un recurso protegido

Conclusión sobre Token-Based Authentication

Este patrón es bastante directo y más fácil de implementar que otros, pero requiere gestionar con cuidado el almacenamiento de los tokens y su revocación. Si tu sistema de e-commerce requiere revocar tokens rápidamente (por ejemplo, cuentas bloqueadas), deberás tener un enfoque central o un broadcast que invalide tokens en todos los nodos.

OAuth (Open Authorization)

Descripción general

OAuth es un protocolo de autorización que permite a los usuarios compartir recursos protegidos con aplicaciones de terceros sin ceder sus credenciales directamente. Con OAuth 2.0, se introdujeron varios flujos (grants), siendo uno de los más comunes el Authorization Code Grant, empleado cuando queremos delegar la autenticación en un proveedor externo (Google, Facebook, GitHub, etc.), o en un servidor de autorización que despleguemos (como Keycloak, Okta, etc.).

Nota: OAuth no es estrictamente un protocolo de autenticación (aunque se use como tal en “login con Google/Facebook”), sino de autorización. Para autenticación real basada en OAuth2, se usa OpenID Connect (OIDC), que añade una capa de identidad sobre OAuth2, proporcionando un ID Token con información del usuario.

Escenario en e-commerce

En este último caso, en lugar de almacenar directamente la contraseña del usuario, redirigimos al usuario a la página de Google para que se autentique. Una vez que Google verifica la identidad, nos devuelve un código que intercambiamos por un access token. Con ese token, consultamos la API de Google para obtener información del perfil (nombre, email, foto, etc.). Acto seguido, creamos o encontramos al usuario en nuestra base de datos interna y le damos acceso.

Configuración en Spring Boot (OAuth2 Client)

La versión de Spring Security 5 (y superiores) simplifica mucho la configuración de OAuth2. Para integrarnos con Google, añadimos a pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
  1. application.yml

En el archivo application.yml (o application.properties), se define la información de registro (registration) y el proveedor (provider).

Un ejemplo con Google es:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: TU_CLIENT_ID_DE_GOOGLE
            client-secret: TU_CLIENT_SECRET_DE_GOOGLE
            scope: 
              - email
              - profile
            redirect-uri: "{baseUrl}/login/oauth2/code/google"
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
            token-uri: https://www.googleapis.com/oauth2/v4/token
            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
  1. Configuración de seguridad

Una vez definidas las propiedades anteriores, creas una clase de configuración que extienda WebSecurityConfigurerAdapter (para versiones de Spring Security < 6.x) o bien uses la nueva aproximación declarativa (desde Spring Boot 3).

Aquí se muestra el enfoque tradicional:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // Autorizamos ciertos endpoints
            .authorizeRequests()
            .antMatchers("/", "/login", "/error").permitAll()
            .anyRequest().authenticated()
            .and()
            // Configuramos el login con OAuth2
            .oauth2Login()
                .defaultSuccessUrl("/home")
                .failureUrl("/login?error=true");
    }
}

De esta forma, Spring Security gestiona el ciclo de OAuth:

ciclo oauth spring security

En un diagrama de flujo sería algo así:

Diagrama flujo oauth security

Uso en microservicios

Podríamos tener un Authorization Server propio, como Keycloak, que maneje a la vez la autenticación y la autorización. En un entorno de microservicios, cada servicio podría:

En un e-commerce, esto es particularmente útil si pretendemos:

Conclusión sobre OAuth

OAuth es poderoso y flexible, pero puede ser más complejo de configurar y mantener, sobre todo si necesitamos manejar nosotros mismos un servidor de autorización. Sin embargo, cuando requerimos integrarnos con proveedores externos o delegar la autenticación a servicios confiables (p. ej., Google), OAuth2 es un estándar prácticamente universal.

En la siguiente entrega

A continuación, veremos el detalle sobre JWT, ejemplo de arquitectura híbrida (OAuth2 + JWT), buenas prácticas y una comparativa bastante interesante.

Te leo en comentarios 👇.

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