Zuul
Zuul
Zuul is an edge service that acts as an API gateway, handling routing, monitoring, and security in a microservice architecture.
- Dynamic routing of incoming requests
- Acts as an API Gateway
- Provides load balancing and rate limiting
- Enhances security through authentication and authorization
Example zuul +Eureka
Route configuration examples
# Route requests with /service1/** to http://localhost:8081/
zuul.routes.service1.path=/service1/**
zuul.routes.service1.url=http://localhost:8081
# Route requests with /service2/** to http://localhost:8082/
zuul.routes.service2.path=/service2/**
zuul.routes.service2.url=http://localhost:8082
# Dynamic routing based on Eureka (if using Eureka)
# Enables service discovery
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka
zuul.routes.service3.path=/service3/**
zuul.routes.service3.serviceId=SERVICE3 # Eureka service ID
Eureka Discovery server
package com.emsi.discoveryserver1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class Discoveryserver1Application {
public static void main(String[] args) {
SpringApplication.run(Discoveryserver1Application.class, args);
}
}
Eureka Discovery server:application.properties
#Port for Registry service
server.port=8761
#Service should not register with itself
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
#Managing the logging
logging.level.com.netflix.eureka=OFF
logging.level.com.netflix.discovery=OFF
Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsi</groupId>
<artifactId>discoveryserver1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>discoveryserver1</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Client :microserviceA :pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsi</groupId>
<artifactId>microserviceclient1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>microserviceclient1</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main
package com.emsi.microserviceclient1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Microserviceclient1Application {
public static void main(String[] args) {
SpringApplication.run(Microserviceclient1Application.class, args);
}
}
application.properties
# Server Config
server.port=8081
# Spring Application Name
spring.application.name=microserviceclient1
# Eureka Client Config
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
Test Rest
package com.emsi.microserviceclient1.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RestC {
@GetMapping("/test")
public String hi()
{
return "hi";
}
}
Zuul gateway
Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version> <!-- Spring Boot 2.1.x -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsi</groupId>
<artifactId>zuulgateway1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuulgateway1</name>
<description>Zuul Gateway for Spring Boot</description>
<properties>
<java.version>11</java.version> <!-- Use Java 11 -->
<spring-cloud.version>Greenwich.SR6</spring-cloud.version> <!-- Spring Cloud Greenwich for Zuul -->
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Zuul Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Spring Boot DevTools (Optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Application.properties
# Server Port
server.port=8086
# Application Name
spring.application.name=zuulgateway1
# Eureka Configuration
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Zuul Routes
zuul.routes.hello.path=/hello/**
zuul.routes.hello.serviceId=microserviceclient1
Main
package com.emsi.zuulgateway1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
public class Zuulgateway1Application {
public static void main(String[] args) {
SpringApplication.run(Zuulgateway1Application.class, args);
}
}
Main
http://localhost:8086/hello/test
Filter
package com.emsi.zuulgateway1.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
@Component
public class CustomZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre"; // This filter will execute before routing the request
}
@Override
public int filterOrder() {
return 1; // Lower values are executed first
}
public boolean shouldFilter() {
return true; // This filter will apply for all requests
}
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Example 1: Log Request Method and URL
System.out.println("Request Method: " + request.getMethod());
System.out.println("Request URL: " + request.getRequestURL().toString());
// Example 2: Authentication Check
String authToken = request.getHeader("Authorization");
if (authToken == null || !isValidToken(authToken)) {
ctx.setResponseStatusCode(401); // Unauthorized
ctx.setResponseBody("Unauthorized access - token is missing or invalid.");
ctx.setSendZuulResponse(false); // Prevent routing the request
return null; // Early exit from filter
}
// Example 3: Modify Request Headers
ctx.addZuulRequestHeader("X-Custom-Header", "CustomValue");
// Example 4: Rate Limiting (Basic Example)
Integer requestCount = (Integer) ctx.get("requestCount");
if (requestCount == null) {
requestCount = 0;
}
requestCount++;
ctx.set("requestCount", requestCount);
// Assuming a simple rate limit of 100 requests
if (requestCount > 100) {
ctx.setResponseStatusCode(429); // Too Many Requests
ctx.setResponseBody("Rate limit exceeded - please try again later.");
ctx.setSendZuulResponse(false); // Prevent routing the request
return null; // Early exit from filter
}
// Example 5: Add Request Time to Header
long startTime = System.currentTimeMillis();
ctx.addZuulRequestHeader("X-Request-Time", String.valueOf(startTime));
// Example 6: Validate Request Parameters
String requiredParam = request.getParameter("requiredParam");
if (requiredParam == null || requiredParam.isEmpty()) {
ctx.setResponseStatusCode(400); // Bad Request
ctx.setResponseBody("Missing required parameter: requiredParam");
ctx.setSendZuulResponse(false); // Prevent routing the request
return null; // Early exit from filter
}
// Example 7: IP Whitelisting
List allowedIPs = Arrays.asList("192.168.1.100", "192.168.1.101"); // Example IPs
String clientIP = request.getRemoteAddr();
if (!allowedIPs.contains(clientIP)) {
ctx.setResponseStatusCode(403); // Forbidden
ctx.setResponseBody("Forbidden access - your IP is not allowed.");
ctx.setSendZuulResponse(false); // Prevent routing the request
return null; // Early exit from filter
}
// Example 8: Service Discovery and Redirection (Custom Logic)
String serviceId = "microserviceclient1";
if (serviceId.equals(request.getParameter("serviceId"))) {
ctx.set("serviceId", serviceId); // Optionally set serviceId in context
}
// Example 9: Modify Request URL Path
String newPath = "/modified-path" + request.getRequestURI();
ctx.set("requestURI", newPath); // Modify the request URI
ctx.set("requestPath", newPath); // Optionally set modified request path in context
return null; // Returning null means no additional processing
}
// Method to validate the token (placeholder logic)
private boolean isValidToken(String token) {
return "valid-token".equals(token); // Replace with actual validation logic
}
}
Netflix Zuul
Introduction à Netflix Zuul
Qu'est-ce que Netflix Zuul ?
Netflix Zuul est un serveur proxy edge et un portail d'API qui fournit un point d'entrée unifié pour tous les clients. Zuul est construit pour permettre le routage dynamique, la surveillance, la résilience et la sécurité des requêtes.
Pourquoi Netflix Zuul ?
- Point d'entrée unique : Un seul point d'accès pour tous les services backend
- Routage dynamique : Routage intelligent vers les bons services
- Filtrage : Inspection et modification des requêtes/réponses
- Sécurité : Authentification et autorisation centralisées
- Résilience : Gestion des pannes et retry automatique
- Monitoring : Observabilité des requêtes et performances
Architecture de Zuul
Architecture typique avec Zuul :
Client Externe → Zuul Gateway → Service Discovery (Eureka) → Services Backend
↑ ↑
Filtres Zuul Load Balancing
Sécurité Health Checks
Routage Configuration
Zuul vs API Gateway Moderne
Zuul 1 vs Zuul 2 vs Spring Cloud Gateway
| Caractéristique | Zuul 1 | Zuul 2 | Spring Cloud Gateway |
|---|---|---|---|
| Architecture | Servlet-based | Reactive (Netty) | Reactive (WebFlux) |
| Performance | Modérée | Élevée | Élevée |
| Threading | Bloquant | Non-bloquant | Non-bloquant |
| Support | Maintenance | Maintenance | Actif |
| Écosystème | Netflix OSS | Netflix OSS | Spring Cloud |
Pourquoi Zuul 1 avec Spring Boot ?
- Stabilité : Mûr et éprouvé en production
- Compatibilité : Bien intégré avec Spring Boot 2.x
- Documentation : Abondante documentation et communauté
- Migration : Facile à migrer vers des solutions modernes
Concepts Fondamentaux de Zuul
Architecture et Composants
Composants Principaux de Zuul
Zuul est composé de plusieurs éléments clés qui travaillent ensemble :
Router :
// Router - Responsable du routage des requêtes
@Component
public class CustomRouter {
private static final Logger logger = LoggerFactory.getLogger(CustomRouter.class);
public void routeRequest(HttpServletRequest request, HttpServletResponse response) {
String requestURI = request.getRequestURI();
String method = request.getMethod();
// Logique de routage personnalisée
String targetService = determineTargetService(requestURI);
String targetPath = determineTargetPath(requestURI);
logger.info("Routing {} {} to service {}", method, requestURI, targetService);
// Implémentation du routage
forwardRequest(request, response, targetService, targetPath);
}
private String determineTargetService(String requestURI) {
// Logique de détermination du service cible
if (requestURI.startsWith("/api/users")) {
return "user-service";
} else if (requestURI.startsWith("/api/orders")) {
return "order-service";
}
return "default-service";
}
private String determineTargetPath(String requestURI) {
// Logique de transformation du chemin
return requestURI.replaceFirst("/api/", "/");
}
private void forwardRequest(HttpServletRequest request, HttpServletResponse response,
String targetService, String targetPath) {
// Implémentation du transfert de requête
// Utilisez RestTemplate, WebClient, ou HTTP client
}
}
Filter :
// Filter - Responsable du filtrage des requêtes/réponses
@Component
public class CustomFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomFilter.class);
@Override
public String filterType() {
// Type de filtre : pre, route, post, error
return "pre";
}
@Override
public int filterOrder() {
// Ordre d'exécution du filtre
return 1;
}
@Override
public boolean shouldFilter() {
// Condition d'application du filtre
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.info("Processing {} request to {}",
request.getMethod(), request.getRequestURL().toString());
// Logique de filtrage
String userAgent = request.getHeader("User-Agent");
if (userAgent != null && userAgent.contains("bot")) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(403);
ctx.setResponseBody("Bot access forbidden");
}
return null;
}
}
Load Balancer :
// Load Balancer - Responsable de la distribution de charge
@Component
public class CustomLoadBalancer {
@Autowired
private LoadBalancerClient loadBalancerClient;
public ServiceInstance chooseService(String serviceName) {
return loadBalancerClient.choose(serviceName);
}
public String getServiceUrl(String serviceName) {
ServiceInstance instance = loadBalancerClient.choose(serviceName);
if (instance != null) {
return "http://" + instance.getHost() + ":" + instance.getPort();
}
throw new RuntimeException("No instance available for service: " + serviceName);
}
public <T> T executeWithLoadBalancer(String serviceName, Function<String, T> operation) {
ServiceInstance instance = loadBalancerClient.choose(serviceName);
if (instance != null) {
String url = "http://" + instance.getHost() + ":" + instance.getPort();
return operation.apply(url);
}
throw new RuntimeException("No instance available for service: " + serviceName);
}
}
Types de Filtres Zuul
- pre :
Exécuté avant le routage. Utilisé pour l'authentification, logging, etc.
- route :
Exécuté pendant le routage. Utilisé pour le routage personnalisé.
- post :
Exécuté après le routage. Utilisé pour modifier la réponse.
- error :
Exécuté lors d'une erreur. Utilisé pour la gestion d'erreurs.
Cycle de Vie d'une Requête
Flux d'Exécution des Filtres
Une requête passe par plusieurs phases de filtrage :
Phases du Cycle de Vie
- Pré-filtrage (pre) :
- Authentification
- Validation des paramètres
- Logging
- Rate limiting
- Routage (route) :
- Détermination de la destination
- Construction de la requête sortante
- Forward vers le service backend
- Post-filtrage (post) :
- Modification de la réponse
- Ajout d'headers
- Compression
- Logging de la réponse
- Gestion d'erreurs (error) :
- Capture des exceptions
- Réponse d'erreur personnalisée
- Logging des erreurs
Exemple de cycle complet :
// Exemple de requête passant par toutes les phases
// 1. Client envoie: GET /api/users/123
// 2. Pré-filtres (pre)
@Order(1)
@Component
public class AuthenticationFilter extends ZuulFilter {
@Override
public String filterType() { return "pre"; }
@Override
public int filterOrder() { return 1; }
@Override
public boolean shouldFilter() { return true; }
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !isValidToken(authHeader)) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("Unauthorized");
}
return null;
}
private boolean isValidToken(String token) {
// Logique de validation du token
return token.startsWith("Bearer ");
}
}
// 3. Pré-filtres (pre) - Logging
@Order(2)
@Component
public class RequestLoggingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public String filterType() { return "pre"; }
@Override
public int filterOrder() { return 2; }
@Override
public boolean shouldFilter() { return true; }
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.info("Incoming request: {} {} from {}",
request.getMethod(),
request.getRequestURL().toString(),
request.getRemoteAddr());
return null;
}
}
// 4. Routage (route) - Routage personnalisé
@Order(1)
@Component
public class CustomRoutingFilter extends ZuulFilter {
@Override
public String filterType() { return "route"; }
@Override
public int filterOrder() { return 1; }
@Override
public boolean shouldFilter() { return true; }
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Routage personnalisé
String serviceUrl = determineServiceUrl(request.getRequestURI());
ctx.setRouteHost(URI.create(serviceUrl));
return null;
}
private String determineServiceUrl(String requestUri) {
if (requestUri.startsWith("/api/users")) {
return "http://user-service:8080";
} else if (requestUri.startsWith("/api/orders")) {
return "http://order-service:8081";
}
return "http://default-service:8082";
}
}
// 5. Post-filtres (post) - Modification de la réponse
@Order(1)
@Component
public class ResponseModificationFilter extends ZuulFilter {
@Override
public String filterType() { return "post"; }
@Override
public int filterOrder() { return 1; }
@Override
public boolean shouldFilter() { return true; }
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
// Ajout d'headers à la réponse
response.setHeader("X-Gateway", "Zuul");
response.setHeader("X-Timestamp", String.valueOf(System.currentTimeMillis()));
return null;
}
}
// 6. Filtres d'erreurs (error) - Gestion des erreurs
@Order(1)
@Component
public class ErrorHandlingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ErrorHandlingFilter.class);
@Override
public String filterType() { return "error"; }
@Override
public int filterOrder() { return 1; }
@Override
public boolean shouldFilter() { return true; }
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
logger.error("Error in Zuul processing", throwable);
ctx.setResponseStatusCode(500);
ctx.setResponseBody("Internal Server Error: " + throwable.getMessage());
ctx.getResponse().setContentType("application/json");
return null;
}
}
Configuration de Base de Zuul
Installation et Dépendances
Ajout des dépendances Maven :
<!-- Core Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Netflix Zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- Eureka Client (optionnel mais recommandé) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Actuator pour monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Configuration de la classe principale :
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy // Active Zuul comme proxy
@EnableDiscoveryClient // Active la découverte de services (optionnel)
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class, args);
}
}
Explication des Annotations
- @EnableZuulProxy :
Active toutes les fonctionnalités de Zuul, y compris le routage dynamique et les filtres.
- @EnableDiscoveryClient :
Active la découverte de services via Eureka ou d'autres registres.
Configuration de base dans application.yml :
# application.yml
server:
port: 8080
spring:
application:
name: zuul-gateway
# Configuration de Zuul
zuul:
# Désactiver les routes par défaut si nécessaire
ignored-services: '*'
# Configuration des routes
routes:
user-service:
path: /api/users/**
serviceId: user-service
strip-prefix: true
order-service:
path: /api/orders/**
serviceId: order-service
strip-prefix: true
# Configuration des timeouts
host:
socket-timeout-millis: 10000
connect-timeout-millis: 5000
# Configuration du routage
semaphore:
max-semaphores: 500
# Désactiver les headers sensibles par défaut
sensitive-headers:
# Configuration Eureka (si utilisé)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: true
fetch-registry: true
# Configuration Actuator
management:
endpoints:
web:
exposure:
include: health,info,gateway,metrics
endpoint:
health:
show-details: always
# Logging
logging:
level:
com.netflix: DEBUG
org.springframework.cloud.netflix.zuul: DEBUG
Explication des Propriétés Clés
- zuul.routes.*.path :
Pattern d'URL pour le routage (ex: /api/users/**).
- zuul.routes.*.serviceId :
Nom du service cible dans Eureka.
- zuul.routes.*.strip-prefix :
Retire le préfixe du chemin lors du routage.
- zuul.host.socket-timeout-millis :
Timeout de lecture en millisecondes (10000 par défaut).
- zuul.host.connect-timeout-millis :
Timeout de connexion en millisecondes (2000 par défaut).
- zuul.sensitive-headers :
Headers sensibles à ne pas transmettre (vide pour tout transmettre).
Premier Démarrage
Construction du projet :
mvn clean package
Lancement du Zuul Gateway :
java -jar target/zuul-gateway-0.0.1-SNAPSHOT.jar
Vérification du démarrage :
GET http://localhost:8080/actuator/health
Validation
Zuul devrait démarrer avec le statut "UP" et être prêt à router les requêtes.
Routage et Filtrage
Configuration du Routage
Routage basique :
# application.yml
zuul:
routes:
# Routage simple par serviceId
user-service:
path: /api/users/**
serviceId: user-service
strip-prefix: true
# Routage avec URL statique
static-service:
path: /api/static/**
url: http://static-service.company.com
strip-prefix: true
# Routage avec regex
user-api:
path: /user/(.*)
serviceId: user-service
strip-prefix: false
# Routage avec préfixe personnalisé
order-api:
path: /orders/**
serviceId: order-service
strip-prefix: true
prefix: /api/v1
Configuration avancée des routes :
# application.yml
zuul:
routes:
# Routage avec retry
user-service:
path: /api/users/**
serviceId: user-service
strip-prefix: true
retryable: true
# Routage avec custom sensitive headers
order-service:
path: /api/orders/**
serviceId: order-service
strip-prefix: true
sensitive-headers: Cookie,Set-Cookie
# Routage ignoré (blacklist)
ignored-service:
path: /api/ignored/**
serviceId: ignored-service
strip-prefix: true
ignored-patterns: /**/admin/**
# Routage avec fallback
fallback-service:
path: /api/fallback/**
serviceId: fallback-service
strip-prefix: true
fallback-uri: forward:/fallback
# Configuration globale des routes
zuul:
ignored-services: '*'
ignored-patterns: /**/admin/**
add-proxy-headers: true
add-host-header: true
retryable: false
Propriétés de Routage
- strip-prefix=true :
Retire le préfixe du chemin lors du routage (ex: /api/users/123 → /users/123).
- retryable=true :
Active le retry automatique sur les erreurs.
- sensitive-headers :
Headers sensibles à ne pas transmettre aux services backend.
- ignored-patterns :
Patterns d'URL à ignorer et ne pas router.
- fallback-uri :
URI de fallback en cas d'erreur de routage.
Filtrage Avancé
Filtres pré-routage (pre) :
@Component
public class PreRoutingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(PreRoutingFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.info("Pre-routing filter: {} {}",
request.getMethod(), request.getRequestURL().toString());
// Ajout de headers personnalisés
ctx.addZuulRequestHeader("X-Forwarded-By", "Zuul-Gateway");
ctx.addZuulRequestHeader("X-Request-Time", String.valueOf(System.currentTimeMillis()));
// Validation des paramètres
String apiKey = request.getHeader("X-API-Key");
if (apiKey == null || !isValidApiKey(apiKey)) {
logger.warn("Invalid or missing API key for request: {}",
request.getRequestURL().toString());
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("{\"error\": \"Unauthorized: Invalid API key\"}");
ctx.getResponse().setContentType("application/json");
}
return null;
}
private boolean isValidApiKey(String apiKey) {
// Implémentation de validation de clé API
return apiKey != null && apiKey.length() > 10;
}
}
Filtres post-routage (post) :
@Component
public class PostRoutingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(PostRoutingFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
logger.info("Post-routing filter: Response status {}",
response.getStatus());
// Ajout de headers de réponse
response.setHeader("X-Processed-By", "Zuul-Gateway");
response.setHeader("X-Response-Time",
String.valueOf(System.currentTimeMillis() - getRequestStartTime(ctx)));
// Modification du corps de réponse
modifyResponseBody(ctx);
return null;
}
private long getRequestStartTime(RequestContext ctx) {
String startTimeStr = ctx.getZuulRequestHeaders().get("X-Request-Time");
if (startTimeStr != null) {
try {
return Long.parseLong(startTimeStr);
} catch (NumberFormatException e) {
return System.currentTimeMillis();
}
}
return System.currentTimeMillis();
}
private void modifyResponseBody(RequestContext ctx) {
if (ctx.getResponseBody() != null) {
String originalBody = ctx.getResponseBody();
String modifiedBody = originalBody.replace("old-value", "new-value");
ctx.setResponseBody(modifiedBody);
}
}
}
Filtres d'erreurs (error) :
@Component
public class ErrorFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getThrowable() != null;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
logger.error("Error in Zuul processing", throwable);
// Logique de gestion d'erreurs personnalisée
if (throwable instanceof HystrixTimeoutException) {
ctx.setResponseStatusCode(408);
ctx.setResponseBody("{\"error\": \"Request Timeout\", \"code\": 408}");
} else if (throwable instanceof ClientException) {
ctx.setResponseStatusCode(503);
ctx.setResponseBody("{\"error\": \"Service Unavailable\", \"code\": 503}");
} else {
ctx.setResponseStatusCode(500);
ctx.setResponseBody("{\"error\": \"Internal Server Error\", \"code\": 500}");
}
ctx.getResponse().setContentType("application/json");
// Nettoyage des ressources
cleanupResources(ctx);
return null;
}
private void cleanupResources(RequestContext ctx) {
// Nettoyage des ressources utilisées
ctx.getZuulRequestHeaders().clear();
ctx.getZuulResponseHeaders().clear();
}
}
Filtrage Dynamique
Filtres conditionnels :
@Component
public class ConditionalFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 2;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Appliquer le filtre seulement pour certaines conditions
String userAgent = request.getHeader("User-Agent");
String requestPath = request.getRequestURI();
// Ne pas appliquer pour les requêtes de health check
if (requestPath.contains("/health") || requestPath.contains("/actuator")) {
return false;
}
// Appliquer seulement pour les requêtes de certaines IP
String clientIp = getClientIpAddress(request);
return isAllowedIp(clientIp);
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Logique de filtrage conditionnel
String clientId = request.getHeader("X-Client-ID");
if (clientId != null) {
// Appliquer des règles spécifiques selon le client
applyClientSpecificRules(ctx, clientId);
}
return null;
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
private boolean isAllowedIp(String ip) {
// Liste blanche d'IP autorisées
Set<String> allowedIps = Set.of("192.168.1.100", "10.0.0.1", "172.16.0.1");
return allowedIps.contains(ip);
}
private void applyClientSpecificRules(RequestContext ctx, String clientId) {
switch (clientId) {
case "premium":
ctx.addZuulRequestHeader("X-Client-Tier", "premium");
ctx.addZuulRequestHeader("X-Rate-Limit", "1000");
break;
case "standard":
ctx.addZuulRequestHeader("X-Client-Tier", "standard");
ctx.addZuulRequestHeader("X-Rate-Limit", "100");
break;
default:
ctx.addZuulRequestHeader("X-Client-Tier", "basic");
ctx.addZuulRequestHeader("X-Rate-Limit", "10");
break;
}
}
}
Filtres configurables :
@Component
@ConfigurationProperties(prefix = "zuul.filters.rate-limit")
public class ConfigurableRateLimitFilter extends ZuulFilter {
private Map<String, Integer> clientLimits = new HashMap<>();
private int defaultLimit = 100;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 3;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String clientId = request.getHeader("X-Client-ID");
int limit = getClientLimit(clientId);
// Implémentation du rate limiting
if (!isWithinRateLimit(clientId, limit)) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Rate limit exceeded\"}");
ctx.getResponse().setContentType("application/json");
}
return null;
}
private int getClientLimit(String clientId) {
return clientLimits.getOrDefault(clientId, defaultLimit);
}
private boolean isWithinRateLimit(String clientId, int limit) {
// Implémentation du rate limiting (simplifiée)
// Dans un vrai système, utilisez Redis, Hazelcast, ou un service de rate limiting
return true; // Simplifié pour l'exemple
}
// Getters et setters pour la configuration
public Map<String, Integer> getClientLimits() {
return clientLimits;
}
public void setClientLimits(Map<String, Integer> clientLimits) {
this.clientLimits = clientLimits;
}
public int getDefaultLimit() {
return defaultLimit;
}
public void setDefaultLimit(int defaultLimit) {
this.defaultLimit = defaultLimit;
}
}
# Configuration dans application.yml
zuul:
filters:
rate-limit:
default-limit: 100
client-limits:
premium: 1000
standard: 500
basic: 100
Filtres Personnalisés
Création de Filtres Basiques
Filtre d'authentification :
@Component
public class AuthenticationFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.debug("Authentication filter: {} {}",
request.getMethod(), request.getRequestURL().toString());
// Vérifier l'authentification
String authHeader = request.getHeader("Authorization");
if (authHeader == null) {
logger.warn("Missing Authorization header for request: {}",
request.getRequestURL().toString());
sendErrorResponse(ctx, 401, "Missing Authorization header");
return null;
}
// Valider le token JWT
if (!isValidJwtToken(authHeader)) {
logger.warn("Invalid JWT token for request: {}",
request.getRequestURL().toString());
sendErrorResponse(ctx, 401, "Invalid or expired token");
return null;
}
// Extraire les claims du token
Map<String, Object> claims = extractClaims(authHeader);
ctx.addZuulRequestHeader("X-User-ID", (String) claims.get("sub"));
ctx.addZuulRequestHeader("X-User-Role", (String) claims.get("role"));
return null;
}
private boolean isValidJwtToken(String authHeader) {
try {
// Implémentation de validation JWT (simplifiée)
// Dans un vrai système, utilisez une bibliothèque comme jjwt
return authHeader.startsWith("Bearer ") && authHeader.length() > 20;
} catch (Exception e) {
logger.error("JWT validation error", e);
return false;
}
}
private Map<String, Object> extractClaims(String authHeader) {
// Implémentation d'extraction des claims (simplifiée)
Map<String, Object> claims = new HashMap<>();
claims.put("sub", "user123");
claims.put("role", "USER");
return claims;
}
private void sendErrorResponse(RequestContext ctx, int statusCode, String message) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(statusCode);
ctx.setResponseBody("{\"error\": \"" + message + "\", \"code\": " + statusCode + "}");
ctx.getResponse().setContentType("application/json");
}
}
Filtre de logging :
@Component
public class RequestLoggingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Log détaillé de la requête
StringBuilder logMessage = new StringBuilder();
logMessage.append("REQUEST: ")
.append(request.getMethod())
.append(" ")
.append(request.getRequestURL().toString())
.append(" from ")
.append(getClientIpAddress(request))
.append(" at ")
.append(new Date());
// Log des headers importants
logMessage.append("\nHeaders:");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (isImportantHeader(headerName)) {
logMessage.append("\n ").append(headerName).append(": ")
.append(request.getHeader(headerName));
}
}
// Log des paramètres de requête
logMessage.append("\nParameters:");
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
logMessage.append("\n ").append(entry.getKey()).append(": ")
.append(Arrays.toString(entry.getValue()));
}
logger.info(logMessage.toString());
// Ajout d'informations de tracing
ctx.addZuulRequestHeader("X-Request-ID", generateRequestId());
ctx.addZuulRequestHeader("X-Request-Start", String.valueOf(System.currentTimeMillis()));
return null;
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
private boolean isImportantHeader(String headerName) {
Set<String> importantHeaders = Set.of(
"User-Agent", "Referer", "Authorization",
"Content-Type", "Accept", "X-Requested-With"
);
return importantHeaders.contains(headerName);
}
private String generateRequestId() {
return UUID.randomUUID().toString();
}
}
Filtre de réponse :
@Component
public class ResponseFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1000;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
// Calcul du temps de réponse
String startTimeStr = ctx.getZuulRequestHeaders().get("X-Request-Start");
long responseTime = 0;
if (startTimeStr != null) {
try {
long startTime = Long.parseLong(startTimeStr);
responseTime = System.currentTimeMillis() - startTime;
} catch (NumberFormatException e) {
logger.warn("Invalid start time format: {}", startTimeStr);
}
}
// Ajout de headers de réponse
response.setHeader("X-Response-Time", responseTime + "ms");
response.setHeader("X-Processed-At", new Date().toString());
// Log de la réponse
logger.info("RESPONSE: {} - Status: {} - Time: {}ms",
ctx.getRequest().getRequestURL().toString(),
response.getStatus(),
responseTime);
return null;
}
}
Filtres Avancés
Filtre de transformation de requête :
@Component
public class RequestTransformationFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestTransformationFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 5;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
return request.getRequestURI().contains("/api/transform/");
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
try {
if ("POST".equalsIgnoreCase(request.getMethod()) ||
"PUT".equalsIgnoreCase(request.getMethod())) {
// Lire le corps de la requête
String requestBody = getRequestBody(request);
if (requestBody != null && !requestBody.isEmpty()) {
// Transformer le corps de la requête
String transformedBody = transformRequestBody(requestBody);
// Remplacer le corps de la requête
setRequestBody(ctx, transformedBody);
logger.debug("Transformed request body from {} to {}",
requestBody.length(), transformedBody.length());
}
}
} catch (Exception e) {
logger.error("Error transforming request body", e);
throw new ZuulException(e, 500, "Error transforming request");
}
return null;
}
private String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
return stringBuilder.toString();
}
private String transformRequestBody(String requestBody) {
// Implémentation de transformation (exemple : conversion de format)
try {
// Exemple : conversion XML → JSON
if (requestBody.trim().startsWith("<")) {
return xmlToJson(requestBody);
}
// Exemple : normalisation JSON
return normalizeJson(requestBody);
} catch (Exception e) {
logger.error("Error transforming request body", e);
return requestBody; // Retourner le corps original en cas d'erreur
}
}
private void setRequestBody(RequestContext ctx, String body) {
ctx.setRequestBody(body);
ctx.addZuulRequestHeader("Content-Length", String.valueOf(body.length()));
}
private String xmlToJson(String xml) {
// Implémentation de conversion XML → JSON
// Utilisez une bibliothèque comme Jackson XML ou JAXB
return "{\"converted\": \"from_xml\"}";
}
private String normalizeJson(String json) {
// Normalisation du JSON (tri des clés, formatage, etc.)
return json.replaceAll("\\s+", " ").trim();
}
}
Filtre de transformation de réponse :
@Component
public class ResponseTransformationFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ResponseTransformationFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 5;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.getResponseDataStream() != null || ctx.getResponseBody() != null;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
try {
// Transformation de la réponse
if (ctx.getResponseBody() != null) {
String responseBody = ctx.getResponseBody();
String transformedResponse = transformResponseBody(responseBody);
ctx.setResponseBody(transformedResponse);
// Mise à jour du Content-Type
ctx.getResponse().setHeader("Content-Type", "application/json");
ctx.getResponse().setHeader("Content-Length",
String.valueOf(transformedResponse.length()));
logger.debug("Transformed response body");
} else if (ctx.getResponseDataStream() != null) {
// Transformation du flux de données
InputStream responseDataStream = ctx.getResponseDataStream();
String responseBody = streamToString(responseDataStream);
String transformedResponse = transformResponseBody(responseBody);
ctx.setResponseDataStream(new ByteArrayInputStream(
transformedResponse.getBytes(StandardCharsets.UTF_8)));
logger.debug("Transformed response data stream");
}
} catch (Exception e) {
logger.error("Error transforming response", e);
throw new ZuulException(e, 500, "Error transforming response");
}
return null;
}
private String transformResponseBody(String responseBody) {
// Implémentation de transformation de réponse
try {
// Exemple : enrichissement des données
if (responseBody.contains("\"users\"")) {
return enrichUserData(responseBody);
}
// Exemple : masquage de données sensibles
return maskSensitiveData(responseBody);
} catch (Exception e) {
logger.error("Error transforming response body", e);
return responseBody; // Retourner la réponse originale en cas d'erreur
}
}
private String enrichUserData(String responseBody) {
// Enrichissement des données utilisateur
return responseBody.replace("\"users\"", "\"enriched_users\"");
}
private String maskSensitiveData(String responseBody) {
// Masquage des données sensibles
return responseBody.replaceAll("(\"password\"\\s*:\\s*\")[^\"]*(\")", "$1***$2")
.replaceAll("(\"ssn\"\\s*:\\s*\")[^\"]*(\")", "$1***$2");
}
private String streamToString(InputStream inputStream) throws IOException {
StringBuilder textBuilder = new StringBuilder();
try (Reader reader = new BufferedReader(
new InputStreamReader(inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
int c;
while ((c = reader.read()) != -1) {
textBuilder.append((char) c);
}
}
return textBuilder.toString();
}
}
Filtre de rate limiting :
@Component
public class RateLimitFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RateLimitFilter.class);
// Simple in-memory rate limiting (à remplacer par Redis en production)
private final Map<String, List<Long>> requestHistory = new ConcurrentHashMap<>();
private final int maxRequestsPerMinute = 60;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 2;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String clientId = getClientIdentifier(request);
long currentTime = System.currentTimeMillis();
// Nettoyer l'historique ancien
cleanupOldRequests(clientId, currentTime);
// Vérifier la limite de requêtes
if (!isWithinRateLimit(clientId, currentTime)) {
logger.warn("Rate limit exceeded for client: {}", clientId);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Rate limit exceeded\", \"retry-after\": 60}");
ctx.getResponse().setContentType("application/json");
return null;
}
// Enregistrer la requête
recordRequest(clientId, currentTime);
return null;
}
private String getClientIdentifier(HttpServletRequest request) {
// Identifier le client (IP + User-Agent)
String ip = getClientIpAddress(request);
String userAgent = request.getHeader("User-Agent");
return ip + ":" + (userAgent != null ? userAgent.hashCode() : "unknown");
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
private void cleanupOldRequests(String clientId, long currentTime) {
List<Long> timestamps = requestHistory.get(clientId);
if (timestamps != null) {
timestamps.removeIf(timestamp -> currentTime - timestamp > 60000); // 1 minute
}
}
private boolean isWithinRateLimit(String clientId, long currentTime) {
List<Long> timestamps = requestHistory.computeIfAbsent(clientId, k -> new ArrayList<>());
return timestamps.size() < maxRequestsPerMinute;
}
private void recordRequest(String clientId, long timestamp) {
requestHistory.computeIfAbsent(clientId, k -> new ArrayList<>()).add(timestamp);
}
}
Sécurité et Authentification
Authentification JWT
Configuration de base :
<!-- Ajout de la dépendance JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Filtre d'authentification JWT :
@Component
public class JwtAuthenticationFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Value("${jwt.secret:mySecretKey}")
private String jwtSecret;
@Value("${jwt.header:Authorization}")
private String jwtHeader;
@Value("${jwt.prefix:Bearer }")
private String jwtPrefix;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Ne pas filtrer les endpoints publics
String requestURI = request.getRequestURI();
return !isPublicEndpoint(requestURI);
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String authHeader = request.getHeader(jwtHeader);
if (authHeader == null || !authHeader.startsWith(jwtPrefix)) {
logger.warn("Missing or invalid JWT token for request: {}",
request.getRequestURL().toString());
sendUnauthorizedError(ctx, "Missing or invalid JWT token");
return null;
}
String token = authHeader.substring(jwtPrefix.length()).trim();
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
// Valider les claims
if (isTokenExpired(claims)) {
sendUnauthorizedError(ctx, "Token expired");
return null;
}
// Ajouter les informations utilisateur aux headers
ctx.addZuulRequestHeader("X-User-ID", claims.getSubject());
ctx.addZuulRequestHeader("X-User-Role", (String) claims.get("role"));
ctx.addZuulRequestHeader("X-User-Email", (String) claims.get("email"));
logger.debug("JWT token validated for user: {}", claims.getSubject());
} catch (JwtException e) {
logger.error("Invalid JWT token", e);
sendUnauthorizedError(ctx, "Invalid JWT token: " + e.getMessage());
}
return null;
}
private boolean isPublicEndpoint(String requestURI) {
return requestURI.endsWith("/login") ||
requestURI.endsWith("/register") ||
requestURI.contains("/public/") ||
requestURI.endsWith("/health") ||
requestURI.contains("/actuator/");
}
private boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
private void sendUnauthorizedError(RequestContext ctx, String message) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("{\"error\": \"" + message + "\", \"code\": 401}");
ctx.getResponse().setContentType("application/json");
}
}
Configuration :
# application.yml
jwt:
secret: ${JWT_SECRET:mySecretKey}
header: Authorization
prefix: Bearer
expiration: 86400 # 24 heures
# Ou via variables d'environnement
# SET JWT_SECRET=your-very-secure-jwt-secret-key
# SET JWT_HEADER=Authorization
# SET JWT_PREFIX=Bearer
OAuth2 et Autorisation
Configuration OAuth2 :
<!-- Ajout de la dépendance OAuth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
Filtre OAuth2 :
@Component
public class OAuth2Filter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(OAuth2Filter.class);
@Value("${oauth2.introspection-uri:}")
private String introspectionUri;
@Value("${oauth2.client-id:}")
private String clientId;
@Value("${oauth2.client-secret:}")
private String clientSecret;
@Autowired
private RestTemplate restTemplate;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String authHeader = request.getHeader("Authorization");
return authHeader != null && authHeader.startsWith("Bearer ");
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String authHeader = request.getHeader("Authorization");
String token = authHeader.substring(7); // "Bearer "
try {
// Introspection du token OAuth2
boolean valid = introspectToken(token);
if (!valid) {
logger.warn("Invalid OAuth2 token for request: {}",
request.getRequestURL().toString());
sendUnauthorizedError(ctx, "Invalid OAuth2 token");
return null;
}
// Ajouter les informations utilisateur
addUserInfoHeaders(ctx, token);
} catch (Exception e) {
logger.error("Error during OAuth2 token introspection", e);
sendUnauthorizedError(ctx, "Error validating OAuth2 token");
}
return null;
}
private boolean introspectToken(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(clientId, clientSecret);
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("token", token);
formData.add("token_type_hint", "access_token");
HttpEntity<MultiValueMap<String, String>> requestEntity =
new HttpEntity<>(formData, headers);
try {
ResponseEntity<Map> response = restTemplate.postForEntity(
introspectionUri, requestEntity, Map.class);
Map<String, Object> responseBody = response.getBody();
return Boolean.TRUE.equals(responseBody.get("active"));
} catch (Exception e) {
logger.error("Token introspection failed", e);
return false;
}
}
private void addUserInfoHeaders(RequestContext ctx, String token) {
// Ajouter les informations utilisateur aux headers
ctx.addZuulRequestHeader("X-OAuth2-Token", token);
ctx.addZuulRequestHeader("X-Auth-Type", "OAuth2");
}
private void sendUnauthorizedError(RequestContext ctx, String message) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("{\"error\": \"" + message + "\", \"code\": 401}");
ctx.getResponse().setContentType("application/json");
}
}
Configuration OAuth2 :
# application.yml
oauth2:
introspection-uri: ${OAUTH2_INTROSPECTION_URI:http://auth-server/oauth/check_token}
client-id: ${OAUTH2_CLIENT_ID:zuul-gateway}
client-secret: ${OAUTH2_CLIENT_SECRET:secret}
# Configuration du RestTemplate
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
# Ou via variables d'environnement
# SET OAUTH2_INTROSPECTION_URI=http://auth-server/oauth/check_token
# SET OAUTH2_CLIENT_ID=zuul-gateway
# SET OAUTH2_CLIENT_SECRET=your-client-secret
Sécurité Avancée
Filtre CORS :
@Component
public class CorsFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CorsFilter.class);
@Value("${cors.allowed-origins:*}")
private String allowedOrigins;
@Value("${cors.allowed-methods:GET,POST,PUT,DELETE,OPTIONS}")
private String allowedMethods;
@Value("${cors.allowed-headers:*}")
private String allowedHeaders;
@Value("${cors.allow-credentials:true}")
private boolean allowCredentials;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -1; // Exécuter avant tous les autres filtres
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
return "OPTIONS".equalsIgnoreCase(request.getMethod());
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
logger.debug("Handling CORS preflight request for: {}",
request.getRequestURL().toString());
// Ajouter les headers CORS
response.setHeader("Access-Control-Allow-Origin", allowedOrigins);
response.setHeader("Access-Control-Allow-Methods", allowedMethods);
response.setHeader("Access-Control-Allow-Headers", allowedHeaders);
response.setHeader("Access-Control-Allow-Credentials", String.valueOf(allowCredentials));
response.setHeader("Access-Control-Max-Age", "3600");
// Terminer la requête OPTIONS
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
return null;
}
}
Filtre Anti-DDoS :
@Component
public class AntiDDoSFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(AntiDDoSFilter.class);
// Tracking des requêtes par IP
private final Map<String, RequestTracker> requestTrackers = new ConcurrentHashMap<>();
@Value("${ddos.max-requests-per-second:100}")
private int maxRequestsPerSecond;
@Value("${ddos.block-duration:300}")
private int blockDuration; // en secondes
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -2; // Très tôt dans la chaîne
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String clientIp = getClientIpAddress(request);
long currentTime = System.currentTimeMillis();
// Vérifier si l'IP est bloquée
if (isBlocked(clientIp, currentTime)) {
logger.warn("Blocked request from blacklisted IP: {}", clientIp);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Too many requests - IP temporarily blocked\"}");
ctx.getResponse().setContentType("application/json");
return null;
}
// Tracker les requêtes
trackRequest(clientIp, currentTime);
// Vérifier le rate limiting
if (isRateLimited(clientIp, currentTime)) {
logger.warn("Rate limited request from IP: {}", clientIp);
// Bloquer temporairement si trop de violations
if (shouldBlock(clientIp)) {
blockIp(clientIp, currentTime);
}
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Too many requests\", \"retry-after\": 60}");
ctx.getResponse().setContentType("application/json");
return null;
}
return null;
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
private boolean isBlocked(String ip, long currentTime) {
RequestTracker tracker = requestTrackers.get(ip);
return tracker != null && tracker.isBlocked(currentTime, blockDuration);
}
private void trackRequest(String ip, long timestamp) {
RequestTracker tracker = requestTrackers.computeIfAbsent(ip, k -> new RequestTracker());
tracker.addRequest(timestamp);
}
private boolean isRateLimited(String ip, long currentTime) {
RequestTracker tracker = requestTrackers.get(ip);
return tracker != null && tracker.getRequestsInLastSecond(currentTime) > maxRequestsPerSecond;
}
private boolean shouldBlock(String ip) {
RequestTracker tracker = requestTrackers.get(ip);
return tracker != null && tracker.getViolationCount() > 10; // 10 violations = blocage
}
private void blockIp(String ip, long currentTime) {
RequestTracker tracker = requestTrackers.get(ip);
if (tracker != null) {
tracker.blockUntil(currentTime + (blockDuration * 1000L));
logger.warn("Temporarily blocked IP: {} for {} seconds", ip, blockDuration);
}
}
// Classe interne pour le tracking des requêtes
private static class RequestTracker {
private final Queue<Long> requestTimestamps = new ConcurrentLinkedQueue<>();
private final AtomicInteger violationCount = new AtomicInteger(0);
private volatile long blockedUntil = 0;
public void addRequest(long timestamp) {
requestTimestamps.offer(timestamp);
cleanupOldRequests(timestamp);
}
public int getRequestsInLastSecond(long currentTime) {
return (int) requestTimestamps.stream()
.filter(timestamp -> currentTime - timestamp < 1000)
.count();
}
public int getViolationCount() {
return violationCount.get();
}
public void incrementViolation() {
violationCount.incrementAndGet();
}
public boolean isBlocked(long currentTime, int blockDuration) {
return currentTime < blockedUntil;
}
public void blockUntil(long timestamp) {
this.blockedUntil = timestamp;
}
private void cleanupOldRequests(long currentTime) {
requestTimestamps.removeIf(timestamp -> currentTime - timestamp > 60000);
}
}
}
Configuration de sécurité :
# application.yml
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:*}
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
ddos:
max-requests-per-second: ${DDOS_MAX_REQUESTS_PER_SECOND:100}
block-duration: ${DDOS_BLOCK_DURATION:300}
# Configuration via variables d'environnement
# SET CORS_ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com
# SET DDOS_MAX_REQUESTS_PER_SECOND=50
# SET DDOS_BLOCK_DURATION=600
Load Balancing
Configuration de Base
Configuration avec Eureka :
# application.yml
zuul:
routes:
user-service:
path: /api/users/**
serviceId: user-service
strip-prefix: true
order-service:
path: /api/orders/**
serviceId: order-service
strip-prefix: true
# Configuration Eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: true
fetch-registry: true
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}:${random.value}
# Configuration du load balancing
ribbon:
ConnectTimeout: 3000
ReadTimeout: 10000
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: false
ServerListRefreshInterval: 2000
Configuration avancée du load balancing :
# application.yml
# Configuration spécifique par service
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
NFLoadBalancerPingClassName: com.netflix.niws.loadbalancer.NIWSDiscoveryPing
ConnectTimeout: 2000
ReadTimeout: 5000
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
order-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
ConnectTimeout: 3000
ReadTimeout: 10000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 3
# Configuration globale Ribbon
ribbon:
ConnectTimeout: 3000
ReadTimeout: 10000
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: false
ServerListRefreshInterval: 2000
eureka:
enabled: true
# Configuration des timeouts Zuul
zuul:
host:
socket-timeout-millis: 10000
connect-timeout-millis: 5000
ribbon:
eager-load:
enabled: true
clients: user-service,order-service
Explication des Propriétés Load Balancing
- ConnectTimeout=3000 :
Timeout de connexion en millisecondes (3 secondes par défaut).
- ReadTimeout=10000 :
Timeout de lecture en millisecondes (10 secondes par défaut).
- MaxAutoRetries=1 :
Nombre de tentatives sur la même instance (1 tentative supplémentaire).
- MaxAutoRetriesNextServer=2 :
Nombre de tentatives sur d'autres instances (2 tentatives supplémentaires).
- OkToRetryOnAllOperations=false :
Ne retenter que sur les requêtes GET (false par défaut).
- ServerListRefreshInterval=2000 :
Intervalle de rafraîchissement de la liste des serveurs (2 secondes).
Stratégies de Load Balancing
Round Robin :
# application.yml
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
# Implémentation personnalisée
@Component
public class CustomRoundRobinRule extends RoundRobinRule {
@Override
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
if (allServers.isEmpty()) {
return null;
}
server = super.choose(lb, key);
if (server == null) {
Thread.yield();
continue;
}
// Vérifier la santé du serveur choisi
if (isServerHealthy(server)) {
return server;
}
server = null;
}
return server;
}
private boolean isServerHealthy(Server server) {
// Implémentation de vérification de santé personnalisée
try {
String healthUrl = "http://" + server.getHost() + ":" + server.getPort() + "/health";
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(healthUrl, String.class);
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
return false;
}
}
}
Weighted Response Time :
# application.yml
order-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
# Configuration personnalisée
@Component
public class CustomWeightedResponseTimeRule extends WeightedResponseTimeRule {
private static final Logger logger = LoggerFactory.getLogger(CustomWeightedResponseTimeRule.class);
@Override
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> allServers = lb.getAllServers();
if (allServers.isEmpty()) {
return null;
}
// Utiliser le weighted response time
server = super.choose(lb, key);
if (server == null) {
Thread.yield();
continue;
}
// Logique personnalisée de vérification
if (shouldSelectServer(server)) {
logger.debug("Selected server {} with weighted response time strategy",
server.getHostPort());
return server;
}
server = null;
}
return server;
}
private boolean shouldSelectServer(Server server) {
// Logique personnalisée de sélection
return server.isAlive() && isServerUnderLoadThreshold(server);
}
private boolean isServerUnderLoadThreshold(Server server) {
// Vérifier si le serveur est sous le seuil de charge
try {
String metricsUrl = "http://" + server.getHost() + ":" + server.getPort() + "/metrics";
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Map> response = restTemplate.getForEntity(metricsUrl, Map.class);
Map<String, Object> metrics = response.getBody();
Double cpuUsage = (Double) metrics.get("cpu.usage");
Double memoryUsage = (Double) metrics.get("memory.usage");
return cpuUsage < 80.0 && memoryUsage < 80.0;
} catch (Exception e) {
logger.warn("Could not check server load for {}", server.getHostPort(), e);
return true; // Accepter le serveur si impossible de vérifier
}
}
}
Load Balancing personnalisé :
@Component
public class ZoneAwareLoadBalancerRule extends AbstractLoadBalancerRule {
private static final Logger logger = LoggerFactory.getLogger(ZoneAwareLoadBalancerRule.class);
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
if (reachableServers.isEmpty()) {
return null;
}
server = chooseServerBasedOnZone(reachableServers);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
return server;
}
private Server chooseServerBasedOnZone(List<Server> servers) {
// Obtenir la zone du client
String clientZone = getClientZone();
// Préférer les serveurs dans la même zone
List<Server> sameZoneServers = servers.stream()
.filter(server -> isSameZone(server, clientZone))
.collect(Collectors.toList());
if (!sameZoneServers.isEmpty()) {
logger.debug("Selecting server from same zone: {}", clientZone);
return sameZoneServers.get(new Random().nextInt(sameZoneServers.size()));
}
// Sinon, choisir parmi tous les serveurs
logger.debug("No servers in same zone, selecting from all available servers");
return servers.get(new Random().nextInt(servers.size()));
}
private String getClientZone() {
// Implémentation de détection de zone
return System.getProperty("zone", "us-east-1a");
}
private boolean isSameZone(Server server, String zone) {
if (server instanceof DiscoveryEnabledServer) {
DiscoveryEnabledServer discoveryServer = (DiscoveryEnabledServer) server;
String serverZone = discoveryServer.getInstanceInfo().getMetadata().get("zone");
return zone.equals(serverZone);
}
return false;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// Initialisation de la configuration
}
}
Retry et Résilience
Configuration des retries :
# application.yml
# Configuration globale des retries
zuul:
retryable: true
ribbon:
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
OkToRetryOnAllOperations: false
retryableStatusCodes: 503,504,408
# Configuration spécifique par service
user-service:
ribbon:
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
retryableStatusCodes: 503,504,408
retryableExceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
- java.util.concurrent.TimeoutException
order-service:
ribbon:
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 3
retryableStatusCodes: 503,504
retryableExceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
# Configuration des timeouts
zuul:
host:
socket-timeout-millis: 10000
connect-timeout-millis: 5000
ribbon:
isolation:
strategy: THREAD
ConnectTimeout: 2000
ReadTimeout: 5000
Filtre de retry personnalisé :
@Component
public class RetryFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RetryFilter.class);
private final Map<String, AtomicInteger> retryCounts = new ConcurrentHashMap<>();
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestId = getRequestId(request);
AtomicInteger retryCount = retryCounts.get(requestId);
return retryCount != null && retryCount.get() > 0;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestId = getRequestId(request);
AtomicInteger retryCount = retryCounts.get(requestId);
if (retryCount != null) {
logger.info("Retry attempt {} for request: {}",
retryCount.get(), requestId);
// Ajouter des headers de retry
ctx.addZuulRequestHeader("X-Retry-Count", String.valueOf(retryCount.get()));
ctx.addZuulRequestHeader("X-Retry-Reason", "Automatic retry");
}
return null;
}
private String getRequestId(HttpServletRequest request) {
String requestId = request.getHeader("X-Request-ID");
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
return requestId;
}
public void incrementRetryCount(String requestId) {
retryCounts.computeIfAbsent(requestId, k -> new AtomicInteger(0)).incrementAndGet();
}
public void resetRetryCount(String requestId) {
retryCounts.remove(requestId);
}
}
// Utilisation dans un service
@Service
public class RetryAwareService {
@Autowired
private RetryFilter retryFilter;
public void handleRetry(String requestId) {
retryFilter.incrementRetryCount(requestId);
}
public void resetRetry(String requestId) {
retryFilter.resetRetryCount(requestId);
}
}
Gestion des erreurs et fallback :
@Component
public class FallbackFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(FallbackFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return -1; // Exécuter avant les autres filtres d'erreur
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
logger.error("Error in Zuul processing", throwable);
// Déterminer le type d'erreur
if (throwable instanceof HystrixTimeoutException) {
handleTimeoutError(ctx);
} else if (throwable instanceof ClientException) {
handleClientError(ctx, (ClientException) throwable);
} else {
handleGenericError(ctx, throwable);
}
return null;
}
private void handleTimeoutError(RequestContext ctx) {
logger.warn("Request timeout occurred, providing fallback response");
ctx.setResponseStatusCode(504);
ctx.setResponseBody("{\"error\": \"Gateway Timeout\", \"code\": 504, \"fallback\": true}");
ctx.getResponse().setContentType("application/json");
// Optionnel : appeler un service de fallback
callFallbackService(ctx);
}
private void handleClientError(RequestContext ctx, ClientException clientException) {
logger.warn("Client error: {}", clientException.getErrorType());
switch (clientException.getErrorType()) {
case SERVER_DOWN:
ctx.setResponseStatusCode(503);
ctx.setResponseBody("{\"error\": \"Service Unavailable\", \"code\": 503}");
break;
case TIMEOUT:
ctx.setResponseStatusCode(504);
ctx.setResponseBody("{\"error\": \"Gateway Timeout\", \"code\": 504}");
break;
default:
ctx.setResponseStatusCode(500);
ctx.setResponseBody("{\"error\": \"Internal Server Error\", \"code\": 500}");
break;
}
ctx.getResponse().setContentType("application/json");
}
private void handleGenericError(RequestContext ctx, Throwable throwable) {
logger.error("Unexpected error occurred", throwable);
ctx.setResponseStatusCode(500);
ctx.setResponseBody("{\"error\": \"Internal Server Error\", \"code\": 500}");
ctx.getResponse().setContentType("application/json");
}
private void callFallbackService(RequestContext ctx) {
try {
String fallbackUrl = "http://fallback-service/fallback";
RestTemplate restTemplate = new RestTemplate();
String fallbackResponse = restTemplate.getForObject(fallbackUrl, String.class);
ctx.setResponseBody(fallbackResponse);
logger.info("Successfully retrieved fallback response");
} catch (Exception e) {
logger.error("Failed to call fallback service", e);
// Utiliser un fallback statique
ctx.setResponseBody("{\"error\": \"Service temporarily unavailable\", \"code\": 503, \"fallback\": true}");
}
}
}
Monitoring et Métriques
Configuration des Métriques
Activation des métriques :
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,gateway
endpoint:
health:
show-details: always
metrics:
enabled: true
prometheus:
enabled: true
gateway:
enabled: true
# Configuration des métriques Zuul
zuul:
metrics:
enabled: true
capture-headers:
enabled: true
request:
- X-Request-ID
- User-Agent
response:
- X-Response-Time
- X-Processed-By
# Configuration Micrometer
management:
metrics:
enable:
zuul: true
distribution:
percentiles-histogram:
zuul: true
percentiles:
zuul:
- 0.5
- 0.9
- 0.95
- 0.99
sla:
zuul:
- 10ms
- 50ms
- 100ms
- 500ms
Configuration Prometheus :
<!-- Ajout de la dépendance Prometheus -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
prometheus:
enabled: true
# Configuration avancée
management:
metrics:
export:
prometheus:
enabled: true
step: 1m
descriptions: true
distribution:
percentiles-histogram:
http:
server:
requests: true
zuul:
requests: true
Endpoints utiles :
# Endpoints de monitoring
GET /actuator/health # Santé de l'application
GET /actuator/metrics # Liste des métriques
GET /actuator/metrics/zuul.requests # Métriques spécifiques Zuul
GET /actuator/prometheus # Endpoint Prometheus
GET /actuator/gateway/routes # Routes configurées
# Exemple de métriques Zuul
zuul.requests.count
zuul.requests.time
zuul.errors.count
zuul.fallback.count
Intégration avec Grafana
Configuration Prometheus :
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'zuul-gateway'
static_configs:
- targets: ['localhost:8080']
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
- job_name: 'services'
static_configs:
- targets: ['localhost:8081', 'localhost:8082']
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
Dashboard Grafana :
# Panel 1: Taux de requêtes
rate(zuul_requests_count[5m])
# Panel 2: Latence 95th percentile
histogram_quantile(0.95, sum(rate(zuul_requests_time_seconds_bucket[5m])) by (le, service))
# Panel 3: Taux d'erreurs
rate(zuul_errors_count[5m])
# Panel 4: Utilisation mémoire
jvm_memory_used_bytes{area="heap"}
# Panel 5: Nombre de threads actifs
jvm_threads_live_threads
# Panel 6: Temps de réponse par service
avg by (service) (zuul_requests_time_seconds_sum / zuul_requests_time_seconds_count)
# Panel 7: Répartition des codes HTTP
sum by (status) (zuul_requests_count)
# Panel 8: Taux de fallback
rate(zuul_fallback_count[5m])
Alertes Prometheus :
# alert-rules.yml
groups:
- name: zuul-alerts
rules:
- alert: HighErrorRate
expr: rate(zuul_errors_count[5m]) / rate(zuul_requests_count[5m]) > 0.1
for: 1m
labels:
severity: warning
annotations:
summary: "High error rate in Zuul gateway"
description: "Error rate above 10% for 1 minute"
- alert: HighLatency
expr: histogram_quantile(0.95, sum(rate(zuul_requests_time_seconds_bucket[5m])) by (le)) > 1
for: 1m
labels:
severity: warning
annotations:
summary: "High latency in Zuul gateway"
description: "95th percentile latency above 1 second"
- alert: ServiceUnavailable
expr: zuul_requests_count{status="503"} > 0
for: 30s
labels:
severity: critical
annotations:
summary: "Service unavailable"
description: "Service returned 503 status code"
Logging et Tracing
Configuration du logging :
# application.yml
logging:
level:
org.springframework.cloud.netflix.zuul: DEBUG
com.netflix.zuul: DEBUG
com.netflix.loadbalancer: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/zuul-gateway.log
max-size: 10MB
max-history: 30
# Configuration avancée du logging
logging:
pattern:
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
file:
name: logs/zuul-gateway.log
max-size: 10MB
max-history: 30
clean-history-on-start: false
Filtre de logging avancé :
@Component
public class AdvancedLoggingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(AdvancedLoggingFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -3; // Très tôt dans la chaîne
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Générer un ID de corrélation
String correlationId = generateCorrelationId(request);
ctx.addZuulRequestHeader("X-Correlation-ID", correlationId);
// Log détaillé
RequestLogEntry logEntry = new RequestLogEntry();
logEntry.setTimestamp(System.currentTimeMillis());
logEntry.setCorrelationId(correlationId);
logEntry.setMethod(request.getMethod());
logEntry.setUrl(request.getRequestURL().toString());
logEntry.setClientIp(getClientIpAddress(request));
logEntry.setUserAgent(request.getHeader("User-Agent"));
logEntry.setHeaders(extractHeaders(request));
logger.info("REQUEST_LOG: {}", logEntry.toJson());
// Stocker l'entrée de log dans le contexte
ctx.set("requestLogEntry", logEntry);
return null;
}
private String generateCorrelationId(HttpServletRequest request) {
String existingId = request.getHeader("X-Correlation-ID");
if (existingId != null && !existingId.isEmpty()) {
return existingId;
}
return UUID.randomUUID().toString();
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
private Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headers.put(headerName, headerValue);
}
return headers;
}
// Classe interne pour l'entrée de log
private static class RequestLogEntry {
private long timestamp;
private String correlationId;
private String method;
private String url;
private String clientIp;
private String userAgent;
private Map<String, String> headers;
// Getters et setters
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public String getCorrelationId() { return correlationId; }
public void setCorrelationId(String correlationId) { this.correlationId = correlationId; }
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getClientIp() { return clientIp; }
public void setClientIp(String clientIp) { this.clientIp = clientIp; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
public Map<String, String> getHeaders() { return headers; }
public void setHeaders(Map<String, String> headers) { this.headers = headers; }
public String toJson() {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(this);
} catch (Exception e) {
return "Error serializing log entry";
}
}
}
}
Filtre de logging post-routage :
@Component
public class ResponseLoggingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(ResponseLoggingFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1000; // Très tard dans la chaîne
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
// Récupérer l'entrée de log du pré-filtre
Object logEntryObj = ctx.get("requestLogEntry");
if (logEntryObj instanceof AdvancedLoggingFilter.RequestLogEntry) {
AdvancedLoggingFilter.RequestLogEntry logEntry =
(AdvancedLoggingFilter.RequestLogEntry) logEntryObj;
// Mettre à jour l'entrée avec les informations de réponse
logEntry.setResponseTime(System.currentTimeMillis() - logEntry.getTimestamp());
logEntry.setResponseCode(ctx.getResponse().getStatus());
logEntry.setResponseSize(ctx.getOriginResponseHeaders().size());
logger.info("RESPONSE_LOG: {}", logEntry.toJson());
}
return null;
}
}
Fonctionnalités Avancées
Rate Limiting Avancé
Configuration avec Redis :
<!-- Ajout de la dépendance Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# application.yml
spring:
redis:
host: localhost
port: 6379
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 2
zuul:
rate-limit:
enabled: true
repository: REDIS
default-policy-list:
- limit: 100
refresh-interval: 60
type:
- user
- origin
key-prefix: zuul-rate-limit
Implémentation personnalisée :
@Component
public class RedisRateLimitFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RedisRateLimitFilter.class);
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Value("${zuul.rate-limit.limit:100}")
private int rateLimit;
@Value("${zuul.rate-limit.window:60}")
private int rateWindow; // en secondes
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String key = generateRateLimitKey(request);
long currentTime = System.currentTimeMillis();
long windowStart = currentTime - (rateWindow * 1000L);
try {
// Incrémenter le compteur
String counterKey = "rate_limit:" + key;
String timestampKey = "rate_limit_timestamps:" + key;
// Ajouter le timestamp actuel
redisTemplate.boundZSetOps(timestampKey).add(currentTime, currentTime);
// Supprimer les timestamps obsolètes
redisTemplate.boundZSetOps(timestampKey).removeRangeByScore(0, windowStart);
// Compter les requêtes dans la fenêtre
Long requestCount = redisTemplate.boundZSetOps(timestampKey).size();
if (requestCount > rateLimit) {
logger.warn("Rate limit exceeded for key: {}", key);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Rate limit exceeded\", \"retry-after\": " + rateWindow + "}");
ctx.getResponse().setContentType("application/json");
return null;
}
logger.debug("Rate limit check passed: {} requests in last {} seconds",
requestCount, rateWindow);
} catch (Exception e) {
logger.error("Error checking rate limit", e);
// En cas d'erreur, autoriser la requête (fail open)
}
return null;
}
private String generateRateLimitKey(HttpServletRequest request) {
String clientIp = getClientIpAddress(request);
String userAgent = request.getHeader("User-Agent");
String apiKey = request.getHeader("X-API-Key");
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(clientIp);
if (apiKey != null) {
keyBuilder.append(":").append(apiKey);
} else if (userAgent != null) {
keyBuilder.append(":").append(userAgent.hashCode());
}
return keyBuilder.toString();
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
Configuration dynamique :
@Configuration
public class RateLimitConfiguration {
@Bean
@ConfigurationProperties(prefix = "zuul.rate-limit")
public RateLimitProperties rateLimitProperties() {
return new RateLimitProperties();
}
@Bean
public RateLimitFilter rateLimitFilter(RateLimitProperties properties) {
return new RateLimitFilter(properties);
}
}
@ConfigurationProperties(prefix = "zuul.rate-limit")
public class RateLimitProperties {
private boolean enabled = true;
private int defaultLimit = 100;
private int defaultWindow = 60;
private String strategy = "IP";
private Map<String, RateLimitRule> rules = new HashMap<>();
// Getters et setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getDefaultLimit() { return defaultLimit; }
public void setDefaultLimit(int defaultLimit) { this.defaultLimit = defaultLimit; }
public int getDefaultWindow() { return defaultWindow; }
public void setDefaultWindow(int defaultWindow) { this.defaultWindow = defaultWindow; }
public String getStrategy() { return strategy; }
public void setStrategy(String strategy) { this.strategy = strategy; }
public Map<String, RateLimitRule> getRules() { return rules; }
public void setRules(Map<String, RateLimitRule> rules) { this.rules = rules; }
public static class RateLimitRule {
private int limit;
private int window;
private String pathPattern;
private List<String> methods = new ArrayList<>();
// Getters et setters
public int getLimit() { return limit; }
public void setLimit(int limit) { this.limit = limit; }
public int getWindow() { return window; }
public void setWindow(int window) { this.window = window; }
public String getPathPattern() { return pathPattern; }
public void setPathPattern(String pathPattern) { this.pathPattern = pathPattern; }
public List<String> getMethods() { return methods; }
public void setMethods(List<String> methods) { this.methods = methods; }
}
}
@Component
public class DynamicRateLimitFilter extends ZuulFilter {
@Autowired
private RateLimitProperties rateLimitProperties;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -2;
}
@Override
public boolean shouldFilter() {
return rateLimitProperties.isEnabled();
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Trouver la règle applicable
RateLimitProperties.RateLimitRule rule = findApplicableRule(request);
if (rule != null) {
applyRateLimit(ctx, request, rule);
} else {
applyDefaultRateLimit(ctx, request);
}
return null;
}
private RateLimitProperties.RateLimitRule findApplicableRule(HttpServletRequest request) {
String requestPath = request.getRequestURI();
String method = request.getMethod();
for (RateLimitProperties.RateLimitRule rule : rateLimitProperties.getRules().values()) {
if (matchesPath(requestPath, rule.getPathPattern()) &&
matchesMethod(method, rule.getMethods())) {
return rule;
}
}
return null;
}
private boolean matchesPath(String path, String pattern) {
if (pattern == null) return true;
return path.matches(pattern);
}
private boolean matchesMethod(String method, List<String> methods) {
if (methods == null || methods.isEmpty()) return true;
return methods.contains(method);
}
private void applyRateLimit(RequestContext ctx, HttpServletRequest request,
RateLimitProperties.RateLimitRule rule) {
// Appliquer la règle de rate limiting
int limit = rule.getLimit();
int window = rule.getWindow();
// Implémentation du rate limiting
if (!checkRateLimit(request, limit, window)) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Rate limit exceeded\", \"limit\": " + limit + ", \"window\": " + window + "}");
ctx.getResponse().setContentType("application/json");
}
}
private void applyDefaultRateLimit(RequestContext ctx, HttpServletRequest request) {
if (!checkRateLimit(request, rateLimitProperties.getDefaultLimit(),
rateLimitProperties.getDefaultWindow())) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(429);
ctx.setResponseBody("{\"error\": \"Rate limit exceeded\", \"limit\": " + rateLimitProperties.getDefaultLimit() + ", \"window\": " + rateLimitProperties.getDefaultWindow() + "}");
ctx.getResponse().setContentType("application/json");
}
}
private boolean checkRateLimit(HttpServletRequest request, int limit, int window) {
// Implémentation du rate limiting
// Utilisez Redis, Hazelcast, ou une solution de rate limiting
return true; // Simplifié pour l'exemple
}
}
Compression et Optimisation
Configuration de la compression :
# application.yml
server:
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,application/javascript,application/json
min-response-size: 1024
zuul:
compression:
enabled: true
mime-types: application/json,application/xml
min-response-size: 2048
gzip:
enabled: true
min-gzip-size: 1024
# Configuration Tomcat
server:
tomcat:
max-threads: 200
min-spare-threads: 10
accept-count: 100
max-connections: 8192
connection-timeout: 20000
Filtre de compression personnalisé :
@Component
public class CompressionFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CompressionFilter.class);
@Value("${zuul.compression.enabled:true}")
private boolean compressionEnabled;
@Value("${zuul.compression.mime-types:application/json,application/xml}")
private List<String> compressibleMimeTypes;
@Value("${zuul.compression.min-response-size:2048}")
private int minResponseSize;
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 999; // Tard dans la chaîne
}
@Override
public boolean shouldFilter() {
return compressionEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
// Vérifier si la réponse est compressible
if (isCompressible(ctx)) {
// Appliquer la compression
compressResponse(ctx);
}
return null;
}
private boolean isCompressible(RequestContext ctx) {
HttpServletResponse response = ctx.getResponse();
String contentType = response.getContentType();
int contentLength = ctx.getOriginResponseHeaders().size();
if (contentType == null) {
return false;
}
// Vérifier le type MIME
boolean mimeTypeSupported = compressibleMimeTypes.stream()
.anyMatch(mimeType -> contentType.startsWith(mimeType));
// Vérifier la taille
boolean sizeSufficient = contentLength >= minResponseSize;
logger.debug("Compression check - MIME: {}, Size: {}, Compressible: {}",
contentType, contentLength, mimeTypeSupported && sizeSufficient);
return mimeTypeSupported && sizeSufficient;
}
private void compressResponse(RequestContext ctx) {
try {
// Ajouter le header de compression
HttpServletResponse response = ctx.getResponse();
response.setHeader("Content-Encoding", "gzip");
// Compresser le corps de la réponse
if (ctx.getResponseBody() != null) {
byte[] compressed = compress(ctx.getResponseBody().getBytes());
ctx.setResponseBody(new String(compressed));
response.setContentLength(compressed.length);
logger.debug("Response compressed, new size: {}", compressed.length);
}
} catch (Exception e) {
logger.error("Error compressing response", e);
}
}
private byte[] compress(byte[] data) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) {
gzip.write(data);
}
return bos.toByteArray();
}
}
Optimisation des performances :
@Configuration
public class PerformanceOptimizationConfig {
@Bean
public ServletRegistrationBean<DispatcherServlet> dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
ServletRegistrationBean<DispatcherServlet> registration =
new ServletRegistrationBean<>(servlet, "/");
registration.setLoadOnStartup(1);
return registration;
}
@Bean
public FilterRegistrationBean<RequestContextListener> requestContextListener() {
FilterRegistrationBean<RequestContextListener> registration =
new FilterRegistrationBean<>(new RequestContextListener());
registration.setUrlPatterns(Arrays.asList("/*"));
return registration;
}
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setProtocol("org.apache.coyote.http11.Http11NioProtocol");
factory.setTomcatConnectorCustomizers(Collections.singletonList(connector -> {
connector.setProperty("maxThreads", "200");
connector.setProperty("minSpareThreads", "10");
connector.setProperty("acceptCount", "100");
connector.setProperty("maxConnections", "8192");
connector.setProperty("connectionTimeout", "20000");
connector.setProperty("keepAliveTimeout", "20000");
connector.setProperty("maxKeepAliveRequests", "100");
}));
return factory;
}
}
@Component
public class PerformanceMonitoringFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -10; // Très tôt dans la chaîne
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Ajouter le timing
ctx.set("startTime", System.currentTimeMillis());
// Optimisation des headers
optimizeHeaders(ctx);
// Optimisation de la mémoire
optimizeMemory(ctx);
return null;
}
private void optimizeHeaders(RequestContext ctx) {
// Optimiser les headers pour la performance
ctx.addZuulRequestHeader("Connection", "keep-alive");
ctx.addZuulRequestHeader("Accept-Encoding", "gzip, deflate");
// Supprimer les headers inutiles
ctx.addZuulRequestHeader("X-Forwarded-Proto", "https");
ctx.addZuulRequestHeader("X-Forwarded-Port", "443");
}
private void optimizeMemory(RequestContext ctx) {
// Optimisation de la mémoire
ctx.set("memoryOptimized", true);
}
}
@Component
public class PerformancePostFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(PerformancePostFilter.class);
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1000; // Très tard dans la chaîne
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
// Calculer le temps de traitement
Long startTime = (Long) ctx.get("startTime");
if (startTime != null) {
long processingTime = System.currentTimeMillis() - startTime;
logger.info("Request processed in {} ms", processingTime);
// Ajouter le timing à la réponse
ctx.getResponse().setHeader("X-Processing-Time", processingTime + "ms");
}
// Libérer les ressources
cleanupResources(ctx);
return null;
}
private void cleanupResources(RequestContext ctx) {
// Nettoyer les ressources
ctx.remove("startTime");
ctx.remove("memoryOptimized");
// Fermer les streams si nécessaire
if (ctx.getResponseDataStream() != null) {
try {
ctx.getResponseDataStream().close();
} catch (IOException e) {
logger.warn("Error closing response stream", e);
}
}
}
}
Cache et Optimisation
Configuration du cache :
<!-- Ajout de la dépendance Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# application.yml
spring:
cache:
type: redis
redis:
time-to-live: 600000 # 10 minutes
cache-null-values: false
redis:
host: localhost
port: 6379
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 2
zuul:
cache:
enabled: true
time-to-live: 300000 # 5 minutes
max-size: 1000
eviction-policy: LRU
Filtre de cache :
@Component
public class CachingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CachingFilter.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${zuul.cache.enabled:true}")
private boolean cacheEnabled;
@Value("${zuul.cache.time-to-live:300000}")
private long cacheTtl;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -5; // Avant les autres filtres
}
@Override
public boolean shouldFilter() {
return cacheEnabled && isCacheableRequest();
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String cacheKey = generateCacheKey(request);
// Vérifier le cache
Object cachedResponse = getCachedResponse(cacheKey);
if (cachedResponse != null) {
logger.info("Cache hit for key: {}", cacheKey);
// Retourner la réponse en cache
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
ctx.setResponseBody(cachedResponse.toString());
ctx.getResponse().setContentType("application/json");
ctx.getResponse().setHeader("X-Cache", "HIT");
ctx.getResponse().setHeader("X-Cache-Key", cacheKey);
return null;
}
// Marquer la requête comme devant être mise en cache
ctx.set("cacheKey", cacheKey);
ctx.set("shouldCache", true);
ctx.getResponse().setHeader("X-Cache", "MISS");
return null;
}
private boolean isCacheableRequest() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Seulement pour les requêtes GET
if (!"GET".equalsIgnoreCase(request.getMethod())) {
return false;
}
// Ne pas mettre en cache les requêtes avec authentification
if (request.getHeader("Authorization") != null) {
return false;
}
// Vérifier les patterns de cache
String requestUri = request.getRequestURI();
return requestUri.contains("/api/public/") ||
requestUri.contains("/api/cacheable/");
}
private String generateCacheKey(HttpServletRequest request) {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(request.getMethod())
.append(":")
.append(request.getRequestURI());
// Ajouter les paramètres de requête
Map<String, String[]> parameterMap = request.getParameterMap();
parameterMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> keyBuilder.append(":")
.append(entry.getKey())
.append("=")
.append(Arrays.toString(entry.getValue())));
return "zuul:cache:" + keyBuilder.toString();
}
private Object getCachedResponse(String cacheKey) {
try {
return redisTemplate.opsForValue().get(cacheKey);
} catch (Exception e) {
logger.error("Error retrieving cached response", e);
return null;
}
}
private void cacheResponse(String cacheKey, Object response) {
try {
redisTemplate.opsForValue().set(cacheKey, response, cacheTtl, TimeUnit.MILLISECONDS);
} catch (Exception e) {
logger.error("Error caching response", e);
}
}
}
Filtre de cache post-routage :
@Component
public class CachePostFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CachePostFilter.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${zuul.cache.enabled:true}")
private boolean cacheEnabled;
@Value("${zuul.cache.time-to-live:300000}")
private long cacheTtl;
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 999; // Tard dans la chaîne
}
@Override
public boolean shouldFilter() {
if (!cacheEnabled) return false;
RequestContext ctx = RequestContext.getCurrentContext();
Boolean shouldCache = (Boolean) ctx.get("shouldCache");
return shouldCache != null && shouldCache;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
String cacheKey = (String) ctx.get("cacheKey");
if (cacheKey != null) {
try {
// Mettre en cache la réponse
String responseBody = ctx.getResponseBody();
if (responseBody != null && ctx.getResponse().getStatus() == 200) {
cacheResponse(cacheKey, responseBody);
ctx.getResponse().setHeader("X-Cache", "STORED");
logger.info("Response cached for key: {}", cacheKey);
}
} catch (Exception e) {
logger.error("Error caching response", e);
}
}
return null;
}
private void cacheResponse(String cacheKey, String response) {
try {
redisTemplate.opsForValue().set(cacheKey, response, cacheTtl, TimeUnit.MILLISECONDS);
} catch (Exception e) {
logger.error("Error caching response", e);
}
}
}
@Component
public class CacheCleanupFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CacheCleanupFilter.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 1000; // Très tard dans la chaîne
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
String cacheKey = (String) ctx.get("cacheKey");
if (cacheKey != null) {
// Nettoyer le cache en cas d'erreur
try {
redisTemplate.delete(cacheKey);
logger.info("Cleaned up cache key due to error: {}", cacheKey);
} catch (Exception e) {
logger.error("Error cleaning up cache", e);
}
}
return null;
}
}
Patterns d'Architecture
Pattern de Gateway
Gateway Pattern
Le Gateway Pattern fournit un point d'entrée unifié pour tous les services backend.
Configuration du Gateway :
# application.yml
zuul:
# Désactiver les routes par défaut
ignored-services: '*'
# Configuration des routes
routes:
user-service:
path: /api/users/**
serviceId: user-service
strip-prefix: true
sensitive-headers:
order-service:
path: /api/orders/**
serviceId: order-service
strip-prefix: true
sensitive-headers:
payment-service:
path: /api/payments/**
serviceId: payment-service
strip-prefix: true
sensitive-headers:
# Route par défaut
default:
path: /**
serviceId: default-service
strip-prefix: true
sensitive-headers:
# Configuration de la sécurité
zuul:
sensitive-headers: Cookie,Set-Cookie,Authorization
add-proxy-headers: true
add-host-header: true
# Configuration des timeouts
zuul:
host:
socket-timeout-millis: 10000
connect-timeout-millis: 5000
# Configuration du routage
zuul:
semaphore:
max-semaphores: 500
ribbon:
eager-load:
enabled: true
clients: user-service,order-service,payment-service
Filtres de Gateway :
@Component
public class GatewayFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(GatewayFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Ajouter des headers de proxy
ctx.addZuulRequestHeader("X-Forwarded-For", getClientIpAddress(request));
ctx.addZuulRequestHeader("X-Forwarded-Proto", request.getScheme());
ctx.addZuulRequestHeader("X-Forwarded-Port", String.valueOf(request.getServerPort()));
// Ajouter l'ID de requête
ctx.addZuulRequestHeader("X-Request-ID", UUID.randomUUID().toString());
logger.info("Gateway processing request: {} {} from {}",
request.getMethod(),
request.getRequestURL().toString(),
getClientIpAddress(request));
return null;
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
Configuration du routage :
@Component
public class RoutingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RoutingFilter.class);
@Override
public String filterType() {
return "route";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Logique de routage personnalisée
String requestURI = request.getRequestURI();
String method = request.getMethod();
// Routage basé sur l'API version
String apiVersion = extractApiVersion(requestURI);
if (apiVersion != null) {
String targetService = determineServiceForVersion(apiVersion, requestURI);
ctx.set("targetService", targetService);
logger.info("Routing to service: {} for API version: {}", targetService, apiVersion);
}
return null;
}
private String extractApiVersion(String requestURI) {
// Extrait la version de l'API de l'URI
// Ex: /api/v1/users → v1
// Ex: /api/v2/users → v2
if (requestURI.matches(".*/api/v\\d+/.*")) {
return requestURI.replaceAll(".*/api/(v\\d+)/.*", "$1");
}
return "v1"; // Version par défaut
}
private String determineServiceForVersion(String version, String requestURI) {
// Détermine le service cible selon la version
if ("v1".equals(version)) {
return "user-service-v1";
} else if ("v2".equals(version)) {
return "user-service-v2";
}
return "user-service"; // Service par défaut
}
}
Pattern de Sécurité
Security Pattern
Le Security Pattern centralise l'authentification et l'autorisation.
Filtre de sécurité :
@Component
public class SecurityFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(SecurityFilter.class);
@Value("${security.enabled:true}")
private boolean securityEnabled;
@Value("${security.auth-endpoint:/api/auth/validate}")
private String authEndpoint;
@Autowired
private RestTemplate restTemplate;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return securityEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Ne pas filtrer les endpoints publics
if (isPublicEndpoint(request.getRequestURI())) {
return null;
}
// Vérifier l'authentification
if (!isAuthenticated(request)) {
logger.warn("Unauthenticated request to: {}", request.getRequestURI());
sendUnauthorizedResponse(ctx);
return null;
}
// Vérifier l'autorisation
if (!isAuthorized(request)) {
logger.warn("Unauthorized request to: {}", request.getRequestURI());
sendForbiddenResponse(ctx);
return null;
}
logger.debug("Security check passed for: {}", request.getRequestURI());
return null;
}
private boolean isPublicEndpoint(String uri) {
return uri.contains("/public/") ||
uri.endsWith("/login") ||
uri.endsWith("/register") ||
uri.contains("/actuator/");
}
private boolean isAuthenticated(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return false;
}
String token = authHeader.substring(7);
return validateToken(token);
}
private boolean isAuthorized(HttpServletRequest request) {
String userRole = (String) RequestContext.getCurrentContext().get("user-role");
String requestURI = request.getRequestURI();
if (userRole == null) {
return false;
}
// Logique d'autorisation
if (requestURI.contains("/admin/")) {
return "ADMIN".equals(userRole);
} else if (requestURI.contains("/user/")) {
return "USER".equals(userRole) || "ADMIN".equals(userRole);
}
return true;
}
private boolean validateToken(String token) {
try {
// Valider le token JWT
// Implémentation simplifiée
return token != null && token.length() > 10;
} catch (Exception e) {
logger.error("Token validation error", e);
return false;
}
}
private void sendUnauthorizedResponse(RequestContext ctx) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("{\"error\": \"Unauthorized\", \"code\": 401}");
ctx.getResponse().setContentType("application/json");
}
private void sendForbiddenResponse(RequestContext ctx) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(403);
ctx.setResponseBody("{\"error\": \"Forbidden\", \"code\": 403}");
ctx.getResponse().setContentType("application/json");
}
}
Filtre d'autorisation :
@Component
public class AuthorizationFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(AuthorizationFilter.class);
@Value("${authorization.enabled:true}")
private boolean authorizationEnabled;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return authorizationEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// Vérifier les permissions
String userRole = (String) ctx.get("user-role");
String userId = (String) ctx.get("user-id");
if (userRole == null || userId == null) {
return null; // Laisser passer si pas d'authentification
}
// Vérifier les permissions RBAC
if (!hasPermission(userRole, request.getRequestURI(), request.getMethod())) {
logger.warn("Access denied for user {} with role {} to {}",
userId, userRole, request.getRequestURI());
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(403);
ctx.setResponseBody("{\"error\": \"Access denied\", \"code\": 403}");
ctx.getResponse().setContentType("application/json");
return null;
}
// Ajouter les headers de sécurité
ctx.addZuulRequestHeader("X-User-ID", userId);
ctx.addZuulRequestHeader("X-User-Role", userRole);
return null;
}
private boolean hasPermission(String userRole, String resource, String action) {
// Implémentation RBAC
return PermissionChecker.hasPermission(userRole, resource, action);
}
}
@Component
public class PermissionChecker {
private static final Map<String, Set<String>> rolePermissions = new HashMap<>();
static {
// Permissions ADMIN
Set<String> adminPermissions = new HashSet<>();
adminPermissions.add("GET:/api/users/**");
adminPermissions.add("POST:/api/users/**");
adminPermissions.add("PUT:/api/users/**");
adminPermissions.add("DELETE:/api/users/**");
adminPermissions.add("GET:/api/orders/**");
adminPermissions.add("POST:/api/orders/**");
adminPermissions.add("PUT:/api/orders/**");
adminPermissions.add("DELETE:/api/orders/**");
rolePermissions.put("ADMIN", adminPermissions);
// Permissions USER
Set<String> userPermissions = new HashSet<>();
userPermissions.add("GET:/api/users/**");
userPermissions.add("POST:/api/users/**");
userPermissions.add("PUT:/api/users/**");
userPermissions.add("GET:/api/orders/**");
userPermissions.add("POST:/api/orders/**");
rolePermissions.put("USER", userPermissions);
}
public static boolean hasPermission(String role, String resource, String action) {
Set<String> permissions = rolePermissions.get(role);
if (permissions == null) {
return false;
}
String permission = action.toUpperCase() + ":" + resource;
return permissions.stream().anyMatch(p -> matchPermission(permission, p));
}
private static boolean matchPermission(String requested, String allowed) {
// Support des wildcards
return requested.matches(allowed.replace("**", ".*").replace("*", "[^/]*"));
}
}
Configuration de sécurité :
# application.yml
security:
enabled: true
auth-endpoint: /api/auth/validate
rbac:
enabled: true
default-role: USER
admin-role: ADMIN
# Configuration des filtres
zuul:
sensitive-headers: Authorization,Cookie,Set-Cookie,X-Auth-Token
add-proxy-headers: true
add-host-header: true
# Configuration SSL
server:
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: password
key-store-type: PKCS12
key-alias: tomcat
# Configuration CORS
zuul:
cors:
allowed-origins: "*"
allowed-methods: GET,POST,PUT,DELETE,OPTIONS
allowed-headers: "*"
allow-credentials: true
Pattern de Résilience
Resilience Pattern
Le Resilience Pattern protège contre les pannes et améliore la disponibilité.
Filtre de circuit breaker :
@Component
public class CircuitBreakerFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerFilter.class);
private final Map<String, CircuitBreaker> circuitBreakers = new ConcurrentHashMap<>();
@Value("${circuit-breaker.enabled:true}")
private boolean circuitBreakerEnabled;
@Value("${circuit-breaker.failure-threshold:5}")
private int failureThreshold;
@Value("${circuit-breaker.timeout:5000}")
private int timeout;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -1;
}
@Override
public boolean shouldFilter() {
return circuitBreakerEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String serviceName = determineServiceName(request.getRequestURI());
if (serviceName == null) {
return null;
}
CircuitBreaker circuitBreaker = getCircuitBreaker(serviceName);
if (circuitBreaker.isOpen()) {
logger.warn("Circuit breaker is OPEN for service: {}", serviceName);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(503);
ctx.setResponseBody("{\"error\": \"Service temporarily unavailable\", \"code\": 503}");
ctx.getResponse().setContentType("application/json");
return null;
}
ctx.set("circuitBreaker", circuitBreaker);
ctx.set("serviceStartTime", System.currentTimeMillis());
return null;
}
private String determineServiceName(String requestURI) {
if (requestURI.startsWith("/api/users/")) {
return "user-service";
} else if (requestURI.startsWith("/api/orders/")) {
return "order-service";
} else if (requestURI.startsWith("/api/payments/")) {
return "payment-service";
}
return null;
}
private CircuitBreaker getCircuitBreaker(String serviceName) {
return circuitBreakers.computeIfAbsent(serviceName, this::createCircuitBreaker);
}
private CircuitBreaker createCircuitBreaker(String serviceName) {
return new CircuitBreaker(serviceName, failureThreshold, timeout);
}
// Classe interne pour le circuit breaker
private static class CircuitBreaker {
private final String serviceName;
private final int failureThreshold;
private final int timeout;
private final Queue<Long> failureTimestamps = new ConcurrentLinkedQueue<>();
private volatile boolean open = false;
private volatile long openTime = 0;
private final long resetTimeout = 30000; // 30 secondes
public CircuitBreaker(String serviceName, int failureThreshold, int timeout) {
this.serviceName = serviceName;
this.failureThreshold = failureThreshold;
this.timeout = timeout;
}
public boolean isOpen() {
if (open) {
// Vérifier si le circuit peut être réouvert
if (System.currentTimeMillis() - openTime > resetTimeout) {
logger.info("Attempting to close circuit breaker for service: {}", serviceName);
open = false;
failureTimestamps.clear();
}
}
return open;
}
public void recordSuccess() {
// Nettoyer les anciens échecs
long cutoff = System.currentTimeMillis() - 60000; // 1 minute
failureTimestamps.removeIf(timestamp -> timestamp < cutoff);
}
public void recordFailure() {
failureTimestamps.offer(System.currentTimeMillis());
// Vérifier si le seuil d'échecs est atteint
if (failureTimestamps.size() >= failureThreshold) {
long cutoff = System.currentTimeMillis() - 60000; // 1 minute
long recentFailures = failureTimestamps.stream()
.filter(timestamp -> timestamp > cutoff)
.count();
if (recentFailures >= failureThreshold) {
open = true;
openTime = System.currentTimeMillis();
logger.warn("Opening circuit breaker for service: {}", serviceName);
}
}
}
}
}
Filtre de retry :
@Component
public class RetryFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RetryFilter.class);
@Value("${retry.enabled:true}")
private boolean retryEnabled;
@Value("${retry.max-attempts:3}")
private int maxAttempts;
@Value("${retry.backoff.base:1000}")
private int baseBackoff;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -2;
}
@Override
public boolean shouldFilter() {
return retryEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String retryCount = request.getHeader("X-Retry-Count");
int currentRetry = retryCount != null ? Integer.parseInt(retryCount) : 0;
ctx.addZuulRequestHeader("X-Retry-Count", String.valueOf(currentRetry));
ctx.addZuulRequestHeader("X-Retry-Attempt", String.valueOf(currentRetry + 1));
if (currentRetry > 0) {
logger.info("Retry attempt {} for request: {}", currentRetry + 1,
request.getRequestURI());
}
return null;
}
}
@Component
public class RetryPostFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(RetryPostFilter.class);
@Value("${retry.enabled:true}")
private boolean retryEnabled;
@Value("${retry.max-attempts:3}")
private int maxAttempts;
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 1000;
}
@Override
public boolean shouldFilter() {
return retryEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
// Vérifier si la réponse est réessayable
if (isRetryableResponse(response.getStatus())) {
String retryCount = ctx.getRequest().getHeader("X-Retry-Count");
int currentRetry = retryCount != null ? Integer.parseInt(retryCount) : 0;
if (currentRetry < maxAttempts - 1) {
// Marquer pour retry
ctx.set("shouldRetry", true);
logger.info("Marking response for retry, status: {}", response.getStatus());
} else {
logger.warn("Max retry attempts reached, sending error response");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(500);
ctx.setResponseBody("{\"error\": \"Max retry attempts reached\", \"code\": 500}");
ctx.getResponse().setContentType("application/json");
}
}
return null;
}
private boolean isRetryableResponse(int statusCode) {
// Codes HTTP réessayables
return statusCode == 503 || statusCode == 504 || statusCode == 408;
}
}
Filtre de fallback :
@Component
public class FallbackFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(FallbackFilter.class);
@Value("${fallback.enabled:true}")
private boolean fallbackEnabled;
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return -1;
}
@Override
public boolean shouldFilter() {
return fallbackEnabled;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
logger.error("Error in Zuul processing", throwable);
// Fournir une réponse de fallback
provideFallbackResponse(ctx, throwable);
return null;
}
private void provideFallbackResponse(RequestContext ctx, Throwable throwable) {
// Déterminer le type d'erreur
if (throwable instanceof HystrixTimeoutException) {
handleTimeoutFallback(ctx);
} else if (throwable instanceof ClientException) {
handleClientExceptionFallback(ctx, (ClientException) throwable);
} else {
handleGenericFallback(ctx);
}
}
private void handleTimeoutFallback(RequestContext ctx) {
logger.warn("Providing timeout fallback response");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(504);
ctx.setResponseBody("{\"error\": \"Gateway Timeout\", \"code\": 504, \"fallback\": true}");
ctx.getResponse().setContentType("application/json");
}
private void handleClientExceptionFallback(RequestContext ctx, ClientException clientException) {
logger.warn("Providing client exception fallback response: {}",
clientException.getErrorType());
switch (clientException.getErrorType()) {
case SERVER_DOWN:
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(503);
ctx.setResponseBody("{\"error\": \"Service Unavailable\", \"code\": 503, \"fallback\": true}");
break;
case TIMEOUT:
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(504);
ctx.setResponseBody("{\"error\": \"Gateway Timeout\", \"code\": 504, \"fallback\": true}");
break;
default:
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(500);
ctx.setResponseBody("{\"error\": \"Internal Server Error\", \"code\": 500, \"fallback\": true}");
break;
}
ctx.getResponse().setContentType("application/json");
}
private void handleGenericFallback(RequestContext ctx) {
logger.warn("Providing generic fallback response");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(500);
ctx.setResponseBody("{\"error\": \"Internal Server Error\", \"code\": 500, \"fallback\": true}");
ctx.getResponse().setContentType("application/json");
}
}
Bonnes Pratiques Zuul
Configuration et Déploiement
Principes Fondamentaux
- Configuration externalisée : Utilisez des fichiers de configuration ou Config Server
- Environnements séparés : Différentes configurations pour dev, test, prod
- Versioning : Versionnez vos configurations avec le code
- Documentation : Documentez clairement les stratégies de sécurité
Configuration Recommandée
# Production
zuul:
host:
socket-timeout-millis: 10000
connect-timeout-millis: 5000
ribbon:
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
ConnectTimeout: 2000
ReadTimeout: 5000
semaphore:
max-semaphores: 500
sensitive-headers: Cookie,Set-Cookie,Authorization
add-proxy-headers: true
add-host-header: true
Performance et Optimisation
Optimisations Clés
- Configuration appropriée : Ajustez les timeouts selon vos besoins spécifiques
- Timeouts raisonnables : Ni trop courts (erreurs prématurées) ni trop longs (ressources bloquées)
- Retry stratégique : Retry seulement sur les erreurs récupérables
- Compression : Activez la compression pour réduire la taille des réponses
Paramètres de Performance Optimisés
| Paramètre | Valeur Recommandée | Raison |
|---|---|---|
| socket-timeout-millis | 10000 | Timeout de lecture raisonnable |
| connect-timeout-millis | 5000 | Timeout de connexion rapide |
| MaxAutoRetries | 1 | 1 tentative supplémentaire |
| MaxAutoRetriesNextServer | 2 | 2 tentatives sur d'autres serveurs |
| semaphore.max-semaphores | 500 | Nombre de requêtes concurrentes |
Sécurité et Résilience
Considérations de Sécurité
- Validation des entrées : Validez toujours les données avant de les traiter
- Chiffrement des communications : Utilisez HTTPS entre les services
- Rate limiting : Protégez contre les abus et DDoS
- Monitoring des accès : Surveillez les patterns d'accès inhabituels
Pièges Courants à Éviter
- Timeouts trop courts : Causent des erreurs prématurées
- Trop de retries : Amplifient les problèmes de performance
- Pas de health checking : Envoient des requêtes à des services défaillants
- Configuration statique : Difficile d'adapter aux conditions changeantes
- Pas de monitoring : Difficile de diagnostiquer les problèmes
Tests et Validation
Stratégies de Test
- Tests unitaires : Testez les configurations de Zuul
- Tests d'intégration : Vérifiez l'interaction avec les services
- Tests de charge : Simulez des scénarios de haute charge
- Tests de chaos : Simulez des pannes de services
- Tests de performance : Mesurez l'impact sur la latence
// Exemple de test de Zuul
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ZuulGatewayTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testRouting() {
// Test de routage
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/users/123", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
@Test
void testSecurity() {
// Test de sécurité
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer invalid-token");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"/api/users/123", HttpMethod.GET, entity, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void testRateLimiting() {
// Test de rate limiting
HttpHeaders headers = new HttpHeaders();
headers.set("X-API-Key", "test-key");
HttpEntity<String> entity = new HttpEntity<>(headers);
// Envoyer plusieurs requêtes rapides
for (int i = 0; i < 100; i++) {
ResponseEntity<String> response = restTemplate.exchange(
"/api/public/data", HttpMethod.GET, entity, String.class);
if (response.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
return;
}
}
fail("Rate limiting should have been triggered");
}
}
Résumé des Bonnes Pratiques
En suivant ces bonnes pratiques, vous obtiendrez :
- Haute disponibilité : Résilience face aux pannes
- Bonne performance : Distribution optimale de la charge
- Facilité de maintenance : Configuration claire et documentée
- Sécurité : Communications sécurisées et contrôlées
- Observabilité : Monitoring et alerting efficaces