Spring Cloud Gateway es la solución oficial de Spring para las API gateways, construida sobre WebFlux para un alto rendimiento no bloqueante. Este documento detalla el establecimiento de una puerta de enlace para microservicios orientada a producción.
Conceptos Fundamentales
- Ruta (Route): Componente básico que comprende un identificador, URI de destino, predicados y filtros.
- Predicado (Predicate): Define condiciones para hacer coincidir las solicitudes entrantes (por ejemplo, ruta, encabezados o parámetros).
- Filtro (Filter): Encargado de procesar las solicitudes y respuestas, aplicando operaciones como autenticación, limitación de tasa o registro de logs.
Configuración Inicial
La dependencia necesaria se añade al archivo de construcción del proyecto:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
La definición de rutas en el archivo de propiedades (application.properties) se estructura de la siguiente manera:
server.port=9090
spring.application.name=mi-gateway
# Configuración de rutas
spring.cloud.gateway.routes[0].id=modulo-usuario
spring.cloud.gateway.routes[0].uri=lb://servicio-usuarios
spring.cloud.gateway.routes[0].predicates[0]=Path=/v1/usuarios/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[1].id=modulo-pedidos
spring.cloud.gateway.routes[1].uri=lb://servicio-pedidos
spring.cloud.gateway.routes[1].predicates[0]=Path=/v1/pedidos/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1
# Habilitar descubrimiento de servicios
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
Filtro Global Personalizado para Autenticación con JWT
Este componente intercepta todas las solicitudes para validar el token JWT, permitiendo el paso libre a endpoints de autenticación.
@Component
public class ValidadorJwtGlobal implements GlobalFilter, Ordered {
private static final String ENCABEZADO_AUTH = "Authorization";
private static final String PREFIJO_BEARER = "Bearer ";
private final String claveSecreta;
public ValidadorJwtGlobal(@Value("${jwt.secret}") String claveSecreta) {
this.claveSecreta = claveSecreta;
}
@Override
public Mono<Void> filter(ServerWebExchange intercambio, GatewayFilterChain cadena) {
RutaHttpSolicitud solicitud = intercambio.getRequest();
String rutaActual = solicitud.getURI().getPath();
// Rutas excluidas del proceso de autenticación
if (rutaActual.startsWith("/publico/") || rutaActual.startsWith("/auth/")) {
return cadena.filter(intercambio);
}
String valorToken = solicitud.getHeaders().getFirst(ENCABEZADO_AUTH);
if (!StringUtils.hasText(valorToken) || !valorToken.startsWith(PREFIJO_BEARER)) {
return rechazarSolicitud(intercambio);
}
try {
String jwt = valorToken.substring(PREFIJO_BEARER.length());
Claims datosToken = parsearJwt(jwt);
String identificadorUsuario = datosToken.getSubject();
// Inyectar identificador en los encabezados para los servicios downstream
RutaHttpSolicitud solicitudModificada = solicitud.mutate()
.header("X-Usuario-Auth", identificadorUsuario)
.build();
return cadena.filter(intercambio.mutate().request(solicitudModificada).build());
} catch (JwtException e) {
return rechazarSolicitud(intercambio);
}
}
private Claims parsearJwt(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(claveSecreta.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
private Mono<Void> rechazarSolicitud(ServerWebExchange intercambio) {
intercambio.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return intercambio.getResponse().setComplete();
}
@Override
public int getOrder() {
return -50; // Alta prioridad
}
}
Implementación de un Filtro de Limitación de Tasa (Rate Limiting)
Un filtro a nivel de ruta que restringe la cantidad de solicitudes por cliente.
@Component
public class LimitadorTasaCliente implements GatewayFilterFactory<ConfigLimitador> {
private final RedisTemplate<String, String> redis;
public LimitadorTasaCliente(RedisTemplate<String, String> redis) {
this.redis = redis;
}
@Override
public GatewayFilter apply(ConfigLimitador config) {
return (intercambio, cadena) -> {
String claveCliente = resolverClaveCliente(intercambio.getRequest());
String claveRedis = "rate_limit:" + claveCliente;
Long conteoActual = redis.opsForValue().increment(claveRedis);
if (conteoActual != null && conteoActual == 1) {
redis.expire(claveRedis, config.getPeriodoSegundos(), TimeUnit.SECONDS);
}
if (conteoActual != null && conteoActual > config.getMaxSolicitudes()) {
intercambio.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return intercambio.getResponse().setComplete();
}
return cadena.filter(intercambio);
};
}
private String resolverClaveCliente(RutaHttpSolicitud solicitud) {
// Priorizar encabezado, luego IP
String cliente = solicitud.getHeaders().getFirst("X-Api-Key");
if (!StringUtils.hasText(cliente)) {
cliente = solicitud.getRemoteAddress() != null ?
solicitud.getRemoteAddress().getAddress().getHostAddress() : "anonimo";
}
return cliente;
}
public static class ConfigLimitador {
private int maxSolicitudes;
private long periodoSegundos;
// Getters y setters
}
}
Para aplicar este filtro en una ruta específica, se configura en el archivo de propiedades:
spring.cloud.gateway.routes[0].filters[1].name=LimitadorTasaCliente
spring.cloud.gateway.routes[0].filters[1].args.maxSolicitudes=50
spring.cloud.gateway.routes[0].filters[1].args.periodoSegundos=60
Gestión de CORS (Intercambio de Recursos de Origen Cruzado)
Configurar un CorsWebFilter permite manejar las políticas de CORS de manera centralizada en la puerta de enlace.
@Configuration
public class ConfiguracionCors {
@Bean
public CorsWebFilter filtroCors() {
CorsConfiguration configuracion = new CorsConfiguration();
configuracion.setAllowedOrigins(List.of("https://app.ejemplo.com"));
configuracion.setAllowedMethods(List.of("GET", "POST", "PUT"));
configuracion.setAllowedHeaders(List.of("Authorization", "Content-Type"));
configuracion.setAllowCredentials(true);
configuracion.setMaxAge(3600L);
UrlBasedCorsConfigurationSource fuente = new UrlBasedCorsConfigurationSource();
fuente.registerCorsConfiguration("/api/**", configuracion);
return new CorsWebFilter(fuente);
}
}
Arquitectura: Relación entre API Gateway y Nginx
Una estrategia común en producción combina ambos componentes para separar responsabilidades:
- Nginx (Capa Exterior): Gestiona la terminación SSL, el balanceo de carga básico de capa 4/7 y el servicio de contenido estático. Actúa como la primera línea de defensa.
- Spring Cloud Gateway (Capa Interior): Se especializa en la lógica de negocio de la API: enrutamiento dinámico basado en predicados, autenticación/autorización (JWT, OAuth2), limitación de tasa contextual, integración con patrones de tolerancia a fallos (Circuit Breaker) y despliegue de funcionalidades por grises.
El flujo típico de una solicitud es: Cliente > Nginx > Spring Cloud Gateway > Servicio Microservicio.
En resumen, Spring Cloud Gateway proporciona un punto de entrada robusto y programable para arquitecturas de microservicios. Su diseño reactivo garantiza eficiencia, mientras que su modelo de filtros ofrece una extensibilidad profunda para implementar políticas transversales de forma centralizada.