Quand un système se décompose en microservices, exposer chacun directement à l’extérieur devient vite ingérable : authentification dupliquée, CORS éparpillé, rate limiting incohérent, logs disséminés. Une API Gateway centralise ces préoccupations transverses et présente un point d’entrée unique aux clients. Dans l’écosystème Spring, Spring Cloud Gateway est la solution officielle. La version 4.3.4 (sortie le 2 avril 2026 dans le train Spring Cloud 2025.0) est compatible Spring Boot 4 et Java 25. Elle expose deux variantes : Server WebFlux (réactive, basée Netty) et Server MVC (synchrone, basée Tomcat). Ce tutoriel reprend la mise en place complète : routes, prédicats, filtres, authentification, rate limiting, et observabilité.
Prérequis
- Java 25 LTS (cf. Virtual threads)
- Spring Boot 4.0+ (cf. Première app Spring Boot)
- 2 microservices à exposer derrière la Gateway (services backend)
- Optionnel : Redis pour rate limiting distribué
- Temps estimé : 90 minutes
Étape 1 — Créer le projet Gateway
Spring Initializr (start.spring.io) propose deux starters distincts depuis Spring Cloud Gateway 4.1 : Reactive Gateway (basé Netty + WebFlux, le défaut historique) et Server Gateway (basé Tomcat + WebMVC, ajouté en 4.1 pour les équipes non-réactives). Le choix dépend de votre stack : si vos microservices sont déjà MVC, prenez Server pour la cohérence.
<!-- pom.xml pour Reactive Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<!-- OU pour MVC Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Pour la suite, on utilise la version reactive (WebFlux) qui reste l’usage majoritaire et offre le meilleur débit pour le pattern Gateway. Le choix MVC s’impose si vous avez besoin de bibliothèques Servlet incompatibles avec Netty (drivers JDBC bloquants dans le pipeline Gateway, par exemple).
Étape 2 — Définir des routes en YAML
Une route Spring Cloud Gateway lie un prédicat (condition de matching) à un URI cible, optionnellement enrichi par des filtres qui transforment la requête/réponse. La configuration YAML est l’approche déclarative la plus lisible pour 90 % des cas.
# application.yml
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: catalogue
uri: http://catalogue-service:8081
predicates:
- Path=/api/catalogue/**
filters:
- RewritePath=/api/catalogue/(?<path>.*), /${path}
- id: commandes
uri: http://commandes-service:8082
predicates:
- Path=/api/commandes/**
- Method=GET,POST,PUT
filters:
- StripPrefix=2
- AddRequestHeader=X-Source, gateway
- id: legacy
uri: https://legacy.example.com
predicates:
- Path=/legacy/**
- Header=X-Migration, true
Trois prédicats clés. Path matche le chemin URL (wildcards * et **). Method restreint aux verbes HTTP listés. Header matche un header avec valeur (utile pour les A/B tests ou migrations). On combine plusieurs prédicats : tous doivent matcher pour activer la route. Pour des prédicats personnalisés (par exemple, matcher un tenant via JWT claim), on écrit une factory custom.
Étape 3 — Filtres : ajouter/réécrire/supprimer
Les filtres transforment la requête entrante ou la réponse sortante. Spring Cloud Gateway en fournit 30+ prêts à l’emploi. Les plus utilisés :
# Filtres prêts à l'emploi
filters:
# Ajouter un header avant de transmettre
- AddRequestHeader=X-Tenant, mon-tenant
# Ajouter un header dans la réponse
- AddResponseHeader=X-Gateway, ITSC
# Supprimer N segments du chemin
- StripPrefix=2 # /api/catalogue/produits → /produits
# Réécrire le chemin avec regex
- RewritePath=/api/v1/(?<path>.*), /v2/${path}
# Ajouter un paramètre query
- AddRequestParameter=correlation_id, abc123
# Retry sur erreurs transitoires
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
methods: GET, POST
backoff:
firstBackoff: 100ms
maxBackoff: 1s
factor: 2
# Circuit breaker (s'intègre avec Resilience4j)
- name: CircuitBreaker
args:
name: catalogueCB
fallbackUri: forward:/fallback/catalogue
# Cache (Reactor Cache)
- name: LocalResponseCache
args:
timeToLive: 30s
Le filtre Retry mérite attention : par défaut Gateway retry les requêtes idempotentes (GET, HEAD) sur erreurs 502/503/504. Activer le retry sur POST/PUT sans idempotency keys côté backend cause des doublons (commande passée 2 fois). Discipline : retry seulement avec idempotency native ou pour des erreurs avec mutation prouvée non-effectuée.
Étape 4 — Routes programmatiques avec RouteLocator
Pour des routes dynamiques (dérivées d’une base de données ou d’un service discovery), la définition programmatique avec RouteLocator offre plus de souplesse que YAML. Idéal pour multi-tenants où chaque tenant a ses propres backends.
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder, TenantConfigService tenantConfig) {
var builderRoutes = builder.routes();
for (Tenant t : tenantConfig.tousActifs()) {
builderRoutes.route(t.getId(), r -> r
.path("/api/" + t.getCode() + "/**")
.filters(f -> f
.stripPrefix(2)
.addRequestHeader("X-Tenant", t.getId())
.addRequestHeader("X-Tier", t.getTier())
.retry(c -> c.setRetries(3))
)
.uri(t.getBackendUrl())
);
}
return builderRoutes.build();
}
}
Pour rafraîchir les routes dynamiquement (sans redémarrage), publier l’événement RefreshRoutesEvent. Cela permet d’ajouter un nouveau tenant en production sans relancer la Gateway. Pour gérer correctement la concurrence pendant le refresh, les requêtes en vol terminent sur l’ancienne config et les nouvelles utilisent la nouvelle.
Étape 5 — Authentification JWT centralisée
La Gateway est l’endroit naturel pour valider les tokens JWT une seule fois plutôt que dans chaque microservice. On utilise spring-boot-starter-oauth2-resource-server qui valide la signature JWT et expose le principal authentifié au reste du pipeline.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
# Configuration
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://identity.example.com/realms/itsc
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
SecurityWebFilterChain chain(ServerHttpSecurity http) {
return http
.csrf(c -> c.disable())
.authorizeExchange(a -> a
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/actuator/health").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
}
# Filtre custom : propager l'identité aux microservices
@Component
public class PropagateAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> (Jwt) ctx.getAuthentication().getPrincipal())
.map(jwt -> exchange.mutate().request(r -> r
.header("X-User-Id", jwt.getSubject())
.header("X-User-Roles", String.join(",", jwt.getClaimAsStringList("roles")))
).build())
.defaultIfEmpty(exchange)
.flatMap(chain::filter);
}
}
Le pattern de propagation par headers permet aux microservices en aval de connaître l’identité utilisateur sans re-valider le JWT eux-mêmes. Discipline critique : les microservices doivent refuser tout appel direct sans passer par la Gateway (firewall réseau ou mTLS), sinon n’importe qui peut injecter le header X-User-Id. Configuration de zero-trust : Gateway en service externe, microservices en réseau privé.
Étape 6 — Rate limiting avec Redis
Pour limiter les appels par utilisateur, IP, ou clé API, Spring Cloud Gateway intègre le filtre RequestRateLimiter basé sur Redis et l’algorithme token bucket. Indispensable pour les API publiques.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
data:
redis:
host: redis
port: 6379
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: api-publique
uri: http://service-public:8083
predicates:
- Path=/api/public/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
@Configuration
public class RateLimitConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> ReactiveSecurityContextHolder.getContext()
.map(ctx -> ((Jwt) ctx.getAuthentication().getPrincipal()).getSubject())
.defaultIfEmpty(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
Le replenishRate (10/sec) définit le débit moyen autorisé, le burstCapacity (20) la rafale autorisée au pic. Pour 60 appels par minute en moyenne avec pics à 20 simultanés, c’est le bon réglage. Au-dessus de la limite, la Gateway renvoie 429 Too Many Requests avec des headers X-RateLimit-Remaining et X-RateLimit-Reset. Pour des limites différenciées (premium vs gratuit), un KeyResolver avec préfixe par tier dans Redis.
Étape 7 — Observabilité : metrics, traces, logs
Une Gateway en production doit exposer ses métriques pour Prometheus, ses traces pour Jaeger/Tempo, et des logs structurés. Spring Cloud Gateway intègre Micrometer (métriques) et Spring Cloud Sleuth/Micrometer Tracing (OpenTelemetry).
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: health, prometheus, gateway
metrics:
distribution:
percentiles-histogram:
spring.cloud.gateway.requests: true
slo:
spring.cloud.gateway.requests: 100ms, 200ms, 500ms, 1s
tracing:
sampling:
probability: 0.1 # 10% sampling en prod
otel:
exporter:
otlp:
endpoint: http://otel-collector:4317
Les métriques clés à grapher en Grafana : spring.cloud.gateway.requests avec dimensions routeId et status (volume + latence par route), resilience4j.circuitbreaker.state (open/closed/half-open par CB), jvm.memory.used (mémoire JVM). Pour le tracing distribué, chaque requête traversant la Gateway porte un traceId propagé aux services en aval — visualisation de bout en bout dans Jaeger.
Étape 8 — Déploiement et scaling
Une Gateway est un goulot par construction : tout le trafic passe par elle. Le scaling se fait horizontalement (multiples replicas derrière un load balancer L4) avec session affinity désactivée (la Gateway est stateless).
# Dockerfile multi-stage
FROM eclipse-temurin:25-jdk AS build
COPY . /app
WORKDIR /app
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:25-jre
COPY --from=build /app/target/gateway-*.jar /app/gateway.jar
ENV JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseZGC"
EXPOSE 8080
ENTRYPOINT exec java $JAVA_OPTS -jar /app/gateway.jar
# Kubernetes : 3 replicas avec HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gateway
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gateway
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
Avec virtual threads activés (spring.threads.virtual.enabled=true) sur la version MVC, ou le modèle reactive natif sur WebFlux, une seule instance Gateway peut gérer 5000-20000 req/s sur 2 vCPU. Le bottleneck devient typiquement la base de données ou les services en aval, pas la Gateway. Pour les pics, scale-out + Redis-backed rate limiting partagé entre instances.
Erreurs fréquentes
| Symptôme | Cause | Solution |
|---|---|---|
| 503 Service Unavailable systématique | URI cible inaccessible | Vérifier DNS, ping, port ; logs Gateway pour erreur exacte |
| Boucle infinie sur route | Path cible matche aussi le prédicat (oubli StripPrefix) | Ajouter StripPrefix= ou réécrire avec RewritePath |
| JWT validé OK mais 401 sur backend | Token non propagé après validation | Vérifier filter qui supprime le header Authorization |
| Rate limit ignoré | Redis injoignable ou KeyResolver null | Vérifier connexion Redis ; logger le key |
| Latence p99 explose à charge moyenne | Pool Netty saturé ou GC pauses | Augmenter reactor.netty.ioWorkerCount ; activer ZGC |
| CORS bloqué | Pas de config CORS Gateway | Ajouter spring.cloud.gateway.globalcors dans application.yml |
Foire aux questions
Spring Cloud Gateway ou Nginx ?
Nginx pour le routage statique simple et terminaison TLS. Spring Cloud Gateway pour le routage métier dynamique avec auth/rate limiting/circuit breaker. Souvent les deux : Nginx en frontal (TLS, static), Gateway derrière (logique métier).
Spring Cloud Gateway ou Kong ?
Kong est une Gateway dédiée riche en plugins (Lua/Go). Plus mature pour les très gros déploiements polyglottes. Spring Cloud Gateway pour les équipes Java homogènes où la Gateway peut partager du code avec les microservices.
Reactive vs MVC Gateway en 2026 ?
Reactive si vous êtes à l’aise avec WebFlux et avez besoin du débit maximum sur peu de cœurs. MVC + virtual threads si vous voulez la simplicité du synchrone avec une perf comparable.
Comment gérer les uploads (gros payloads) ?
Augmenter spring.webflux.max-in-memory-size ou utiliser le streaming directement (pas de buffering Gateway). Pour des fichiers très lourds (> 100 Mo), URLs signées S3/R2 et bypass de la Gateway.
Combien de routes max ?
Plusieurs centaines sans souci sur la perf. Au-delà, basculer vers une logique de dispatch dynamique (RouteLocator basé sur dictionnaire ou Redis).
Pour aller plus loin
La Gateway en place, l’étape suivante consiste à durcir la résilience des appels en aval avec Resilience4j. Vue panoramique : Java Enterprise moderne.