تطوير الويب

Spring Cloud Gateway 4.3: routes، filters، JWT وrate limiting

7 دقائق للقراءة

دروس السلسلة: Virtual threads Java 25 · Spring Boot 4 + GraalVM Native · JPA Hibernate المتقدم · Resilience4j 3

عندما يتفكّك نظام إلى microservices، كشف كل واحد مباشرة للخارج يصبح سريعًا غير قابل للإدارة: مصادقة مُكرَّرة، CORS مُشتَّت، rate limiting غير متماسك، logs مُبَعثَرة. API Gateway تُمركز هذه الاهتمامات المتقاطعة وتُقدّم نقطة دخول وحيدة للعملاء. في منظومة Spring، Spring Cloud Gateway هو الحل الرسمي. الإصدار 4.3.4 (الصادر 2 أبريل 2026 في قطار Spring Cloud 2025.0) متوافق مع Spring Boot 4 وJava 25. يكشف نسختين: Server WebFlux (تفاعلية، مبنية Netty) وServer MVC (متزامنة، مبنية Tomcat).

المتطلبات

  • Java 25 LTS
  • Spring Boot 4.0+
  • 2 microservices للكشف خلف Gateway
  • اختياري: Redis لـ rate limiting موزّع
  • الوقت المُقدَّر: 90 دقيقة

الخطوة 1 — إنشاء مشروع Gateway

Spring Initializr يقترح starters مميزين منذ Spring Cloud Gateway 4.1: Reactive Gateway (مبني Netty + WebFlux، الافتراضي التاريخي) وServer Gateway (مبني Tomcat + WebMVC، مُضاف في 4.1 للفرق غير التفاعلية).

<!-- pom.xml pour Reactive Gateway -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>

<!-- OU pour MVC Gateway -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>
</dependency>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2025.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

للبقية، نستخدم النسخة reactive (WebFlux) التي تبقى الاستخدام الأغلب وتُقدّم أفضل throughput لنمط Gateway. اختيار MVC ينفرض إذا تحتاج مكتبات Servlet غير متوافقة مع Netty.

الخطوة 2 — تعريف routes في YAML

route في Spring Cloud Gateway يربط prédicat (شرط matching) بـ URI هدف، اختياريًا مُثرى بـ filters تُحوّل الطلب/الرد.

# application.yml
spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: catalogue
              uri: http://catalogue-service:8081
              predicates:
                - Path=/api/catalogue/**
              filters:
                - RewritePath=/api/catalogue/(?<path>.*), /\${path}

            - id: commandes
              uri: http://commandes-service:8082
              predicates:
                - Path=/api/commandes/**
                - Method=GET,POST,PUT
              filters:
                - StripPrefix=2
                - AddRequestHeader=X-Source, gateway

            - id: legacy
              uri: https://legacy.example.com
              predicates:
                - Path=/legacy/**
                - Header=X-Migration, true

ثلاثة prédicats مفتاحية. Path يُطابق مسار URL (wildcards * و**). Method يُقَيِّد بأفعال HTTP المسرودة. Header يُطابق header بقيمة (مفيد لـ A/B tests أو الترحيلات).

الخطوة 3 — Filters: إضافة/إعادة كتابة/حذف

filters:
  # Ajouter un header avant de transmettre
  - AddRequestHeader=X-Tenant, mon-tenant

  # Ajouter un header dans la réponse
  - AddResponseHeader=X-Gateway, ITSC

  # Supprimer N segments du chemin
  - StripPrefix=2   # /api/catalogue/produits → /produits

  # Réécrire le chemin avec regex
  - RewritePath=/api/v1/(?<path>.*), /v2/\${path}

  # Ajouter un paramètre query
  - AddRequestParameter=correlation_id, abc123

  # Retry sur erreurs transitoires
  - name: Retry
    args:
      retries: 3
      statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
      methods: GET, POST
      backoff:
        firstBackoff: 100ms
        maxBackoff: 1s
        factor: 2

  # Circuit breaker (s'intègre avec Resilience4j)
  - name: CircuitBreaker
    args:
      name: catalogueCB
      fallbackUri: forward:/fallback/catalogue

  # Cache (Reactor Cache)
  - name: LocalResponseCache
    args:
      timeToLive: 30s

الـ Retry يستحق الانتباه: افتراضيًا، Gateway يُعيد المحاولة للطلبات idempotente (GET، HEAD) على أخطاء 502/503/504. تفعيل retry على POST/PUT دون idempotency keys على جانب backend يُسبّب تكرارات (طلب مرّر مرتين).

الخطوة 4 — Routes برمجية بـ RouteLocator

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder, TenantConfigService tenantConfig) {
        var builderRoutes = builder.routes();

        for (Tenant t : tenantConfig.tousActifs()) {
            builderRoutes.route(t.getId(), r -> r
                .path("/api/" + t.getCode() + "/**")
                .filters(f -> f
                    .stripPrefix(2)
                    .addRequestHeader("X-Tenant", t.getId())
                    .addRequestHeader("X-Tier", t.getTier())
                    .retry(c -> c.setRetries(3))
                )
                .uri(t.getBackendUrl())
            );
        }

        return builderRoutes.build();
    }
}

لتحديث routes ديناميكيًا (بدون إعادة تشغيل)، نشر حدث RefreshRoutesEvent. يُتيح إضافة tenant جديد في الإنتاج دون إعادة إطلاق Gateway.

الخطوة 5 — مصادقة JWT مركزية

Gateway هو المكان الطبيعي للتحقق من tokens JWT مرة واحدة بدل في كل microservice. نستخدم spring-boot-starter-oauth2-resource-server.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://identity.example.com/realms/itsc

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
    @Bean
    SecurityWebFilterChain chain(ServerHttpSecurity http) {
        return http
            .csrf(c -> c.disable())
            .authorizeExchange(a -> a
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/actuator/health").permitAll()
                .anyExchange().authenticated()
            )
            .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
            .build();
    }
}

@Component
public class PropagateAuthFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return ReactiveSecurityContextHolder.getContext()
            .map(ctx -> (Jwt) ctx.getAuthentication().getPrincipal())
            .map(jwt -> exchange.mutate().request(r -> r
                .header("X-User-Id", jwt.getSubject())
                .header("X-User-Roles", String.join(",", jwt.getClaimAsStringList("roles")))
            ).build())
            .defaultIfEmpty(exchange)
            .flatMap(chain::filter);
    }
}

نمط النشر بـ headers يُتيح للـ microservices المخدومة معرفة هوية المستخدم دون إعادة التحقق من JWT بأنفسها. انضباط حرج: الـ microservices يجب أن ترفض أي استدعاء مباشر دون المرور بـ Gateway (firewall شبكي أو mTLS).

الخطوة 6 — Rate limiting بـ Redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

spring:
  data:
    redis:
      host: redis
      port: 6379

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: api-publique
              uri: http://service-public:8083
              predicates:
                - Path=/api/public/**
              filters:
                - name: RequestRateLimiter
                  args:
                    redis-rate-limiter.replenishRate: 10
                    redis-rate-limiter.burstCapacity: 20
                    redis-rate-limiter.requestedTokens: 1
                    key-resolver: "#{@userKeyResolver}"

@Configuration
public class RateLimitConfig {
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> ReactiveSecurityContextHolder.getContext()
            .map(ctx -> ((Jwt) ctx.getAuthentication().getPrincipal()).getSubject())
            .defaultIfEmpty(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

الـ replenishRate (10/ث) يُحدّد المعدّل المتوسط المسموح، burstCapacity (20) الـ rafale المسموحة. فوق الحد، Gateway يُرجع 429 Too Many Requests.

الخطوة 7 — الملاحظة: metrics وtraces وlogs

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

management:
  endpoints:
    web:
      exposure:
        include: health, prometheus, gateway
  metrics:
    distribution:
      percentiles-histogram:
        spring.cloud.gateway.requests: true
      slo:
        spring.cloud.gateway.requests: 100ms, 200ms, 500ms, 1s
  tracing:
    sampling:
      probability: 0.1   # 10% sampling en prod

otel:
  exporter:
    otlp:
      endpoint: http://otel-collector:4317

Metrics مفتاحية للرسم في Grafana: spring.cloud.gateway.requests بأبعاد routeId وstatus، resilience4j.circuitbreaker.state، jvm.memory.used.

الخطوة 8 — النشر والتوسّع

# Dockerfile multi-stage
FROM eclipse-temurin:25-jdk AS build
COPY . /app
WORKDIR /app
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:25-jre
COPY --from=build /app/target/gateway-*.jar /app/gateway.jar
ENV JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseZGC"
EXPOSE 8080
ENTRYPOINT exec java $JAVA_OPTS -jar /app/gateway.jar

# Kubernetes : 3 replicas avec HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gateway
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: gateway
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

مع virtual threads مُفعَّلة أو النموذج reactive الأصلي، instance Gateway واحد يستطيع إدارة 5000-20000 req/ث على 2 vCPU.

أخطاء شائعة

العَرَض السبب الحل
503 Service Unavailable منهجيًا URI هدف غير متاح تحقّق من DNS، ping، المنفذ؛ logs Gateway
حلقة لا نهائية على route Path الهدف يُطابق أيضًا الـ prédicat أضف StripPrefix أو RewritePath
JWT صحيح لكن 401 على backend Token غير مُنشَر بعد التحقق تحقّق من filter يحذف header Authorization
Rate limit متجاهَل Redis غير متاح أو KeyResolver null تحقّق من اتصال Redis
latency p99 تنفجر Pool Netty مُشبَع أو GC pauses زِد reactor.netty.ioWorkerCount؛ فعّل ZGC
CORS محجوب لا config CORS Gateway أضف spring.cloud.gateway.globalcors

الأسئلة الشائعة

Spring Cloud Gateway أم Nginx؟
Nginx للتوجيه الثابت البسيط وإنهاء TLS. Spring Cloud Gateway للتوجيه المهني الديناميكي مع auth/rate limiting/circuit breaker. غالبًا الاثنان: Nginx في الأمام، Gateway خلف.

Spring Cloud Gateway أم Kong؟
Kong gateway مخصصة غنية بـ plugins (Lua/Go). أكثر نضجًا للنشرات الكبيرة جدًا. Spring Cloud Gateway للفرق Java متجانسة حيث Gateway يُشارك كود مع microservices.

Reactive vs MVC Gateway في 2026؟
Reactive إذا أنت مرتاح مع WebFlux وتحتاج throughput أقصى. MVC + virtual threads إذا تريد بساطة المتزامن بأداء قابل للمقارنة.

كيف نُدير uploads (payloads كبيرة)؟
زِد spring.webflux.max-in-memory-size أو استخدم streaming مباشرة. للملفات الثقيلة (> 100 MB)، URLs موقّعة S3/R2 وbypass Gateway.

كم routes أقصى؟
عدة مئات بلا مشكلة على الأداء. ما بعد ذلك، انتقل لمنطق dispatch ديناميكي.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité