Zuul

Zuul

Zuul is an edge service that acts as an API gateway, handling routing, monitoring, and security in a microservice architecture.

  1. Dynamic routing of incoming requests
  2. Acts as an API Gateway
  3. Provides load balancing and rate limiting
  4. 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 :

1

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
    }
}
2

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;
    }
}
3

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

  1. Pré-filtrage (pre) :
    • Authentification
    • Validation des paramètres
    • Logging
    • Rate limiting
  2. Routage (route) :
    • Détermination de la destination
    • Construction de la requête sortante
    • Forward vers le service backend
  3. Post-filtrage (post) :
    • Modification de la réponse
    • Ajout d'headers
    • Compression
    • Logging de la réponse
  4. Gestion d'erreurs (error) :
    • Capture des exceptions
    • Réponse d'erreur personnalisée
    • Logging des erreurs
1

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

1

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

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.

3

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

1

Construction du projet :

mvn clean package
2

Lancement du Zuul Gateway :

java -jar target/zuul-gateway-0.0.1-SNAPSHOT.jar
3

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

1

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
2

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é

1

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;
    }
}
2

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);
        }
    }
}
3

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

1

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;
        }
    }
}
2

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

1

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");
    }
}
2

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();
    }
}
3

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

1

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();
    }
}
2

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();
    }
}
3

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

1

Configuration de base :

<!-- Ajout de la dépendance JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
2

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");
    }
}
3

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

1

Configuration OAuth2 :

<!-- Ajout de la dépendance OAuth2 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
2

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");
    }
}
3

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

1

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;
    }
}
2

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);
        }
    }
}
3

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

1

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
2

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

1

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;
        }
    }
}
2

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
        }
    }
}
3

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

1

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
2

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);
    }
}
3

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

1

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
2

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
3

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

1

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
2

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])
3

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

1

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
2

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";
            }
        }
    }
}
3

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é

1

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
2

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();
    }
}
3

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

1

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
2

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();
    }
}
3

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

1

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
2

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);
        }
    }
}
3

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.

1

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
2

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();
    }
}
3

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.

1

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");
    }
}
2

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("*", "[^/]*"));
    }
}
3

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é.

1

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);
                }
            }
        }
    }
}
2

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;
    }
}
3

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