اكتشاف الخدمات والإعداد والبوّابة

Spring Cloud Gateway

18 دقيقة الدرس 7 من 12

Spring Cloud Gateway

في الدرس السابق تعلّمت الحجة النظرية لوجود API Gateway. هذا الدرس عملي بالكامل: ستبني بوابة تعمل فعلياً باستخدام Spring Cloud Gateway (التنفيذ التفاعلي غير المحجوب المبني على Spring WebFlux ومشروع Reactor). بنهاية الدرس ستفهم كيف تُعلن المسارات، وتُركّب الشروط، وتُطبّق الفلاتر، وتتعامل مع التداعيات الأمنية والتشغيلية لكل قرار.

إضافة التبعية

Spring Cloud Gateway هو مشغّل (starter) منفصل. أنشئ مشروع Spring Boot 3 جديداً (أو وحدة) وأضفه إلى pom.xml:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
لا تُضف spring-boot-starter-web إلى جانب مشغّل البوابة. Spring Cloud Gateway مبني على WebFlux (تفاعلي وغير محجوب). دمجه مع spring-boot-starter-web القائم على Servlet في نفس مسار الفئات يُسبب تعارضاً عند بدء التشغيل. استخدم مشغّل البوابة فقط؛ إذا احتجت الأمان أضف spring-boot-starter-security وسيُضبط المتغير التفاعلي من Spring Security تلقائياً.

المسار: اللبنة الأساسية

يمثّل المسار (Route) المفهوم الجوهري في Spring Cloud Gateway. لكل مسار ثلاثة أجزاء:

  1. المعرّف (ID) — سلسلة نصية فريدة تُستخدم في السجلات والمقاييس.
  2. عنوان URI — الوجهة التي يُوجَّه إليها الطلب عند المطابقة.
  3. الشروط (Predicates) — شروط يجب أن تتحقق جميعها لكي يتطابق المسار مع الطلب.
  4. الفلاتر (Filters) — تحويلات تُطبَّق على الطلب قبل إعادة توجيهه أو على الاستجابة قبل إعادتها.

يمكن إعلان المسارات في application.yml (الأكثر شيوعاً)، أو في حبة Java من نوع RouteLocator، أو ديناميكياً عبر عميل الاكتشاف. ابدأ بـ YAML:

spring: cloud: gateway: routes: - id: order-service-route uri: http://localhost:8081 predicates: - Path=/api/orders/** filters: - StripPrefix=1

ما يفعله هذا: أي طلب يبدأ مساره بـ /api/orders/ يُوجَّه إلى http://localhost:8081. يُزيل فلتر StripPrefix=1 المقطع الأول من المسار (/api) قبل إعادة التوجيه، فيتحوّل الطلب GET /api/orders/42 إلى GET /orders/42 عند الخدمة الخلفية.

الشروط (Predicates) بالتفصيل

الشرط هو اختبار منطقي على كائن ServerWebExchange الوارد. يأتي Spring Cloud Gateway مزوَّداً بمجموعة غنية من الشروط المدمجة. الشروط المتعددة على مسار واحد تُجمع بعملية AND.

شرط Path — يطابق على مسار URL باستخدام أحرف Ant البديلة:

predicates: - Path=/api/products/**, /api/catalog/**

شرط Method — يُقيّد بفعل HTTP:

predicates: - Method=GET,HEAD

شرط Header — يطابق عندما يوجد رأس وقيمته تستوفي تعبيراً نمطياً:

predicates: - Header=X-Request-Source, internal-.*

شرط Query — يطابق عندما يوجد معامل استعلام (ويطابق تعبيراً نمطياً اختيارياً):

predicates: - Query=version, v[23]

شروط After / Before / Between — توجيه ذو نافذة زمنية، مفيد للصيانة المجدولة أو علامات الميزات:

predicates: - Between=2024-01-01T00:00:00+00:00[UTC], 2025-01-01T00:00:00+00:00[UTC]

شرط Weight — يُستخدم لنشر canary؛ يُوجّه نسبة مئوية من حركة المرور إلى إصدار جديد:

spring: cloud: gateway: routes: - id: product-service-v1 uri: http://localhost:8082 predicates: - Path=/api/products/** - Weight=product-group, 90 - id: product-service-v2 uri: http://localhost:8083 predicates: - Path=/api/products/** - Weight=product-group, 10
ترتيب الشروط مهم. تُقيَّم المسارات بترتيب الإعلان وأول مطابقة تفوز. ضع المسارات الأكثر تحديداً قبل الأعم، تماماً كما تفعل مع كتل catch في Java. يجب أن يكون مسار الاصطياد الشامل Path=/** دائماً في النهاية.

فلاتر البوابة (Gateway Filters) بالتفصيل

تعمل الفلاتر على كائن التبادل. يُطبَّق GatewayFilter على مسار واحد؛ ويُطبَّق GlobalFilter على كل مسار. تغطي الفلاتر المدمجة أكثر احتياجات البوابة شيوعاً.

AddRequestHeader — حقن رأس قبل إعادة التوجيه:

filters: - AddRequestHeader=X-Gateway-Source, my-gateway

تستطيع الخدمات الخلفية التحقق من هذا الرأس للتأكد من أن الطلبات مرّت عبر البوابة لا مباشرةً.

AddResponseHeader — حقن رأس في طريق العودة:

filters: - AddResponseHeader=X-Content-Type-Options, nosniff

RewritePath — إعادة كتابة المسار بتعبيرات نمطية كاملة، أقوى من StripPrefix:

filters: - RewritePath=/api/v1/(?<segment>.*), /${segment}

RequestRateLimiter — تحديد معدل الطلبات المدمج عبر خوارزمية دلو الرموز مع Redis. أضف مشغّل Redis التفاعلي ثم اضبط:

filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 50 # رموز تُضاف في الثانية redis-rate-limiter.burstCapacity: 100 # أقصى رموز في الدلو redis-rate-limiter.requestedTokens: 1 # رموز تُستهلك لكل طلب key-resolver: "#{@ipKeyResolver}" # مرجع حبة Spring SpEL

يحدّد مُحلل المفتاح (key resolver) كيفية تمييز المُستدعي. أبسط مُحلل يستخدم عنوان IP البعيد:

package com.example.gateway.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; @Configuration public class RateLimiterConfig { @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just( exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() ); } }

في الإنتاج، قم بالتحليل على موضوع JWT أو مفتاح API بدلاً من IP كي لا تتشارك عناوين NAT نفس الدلو بشكل غير عادل.

فلتر CircuitBreaker — يتكامل مع Resilience4j لفتح الدائرة عند فشل الخدمة الخلفية:

filters: - name: CircuitBreaker args: name: order-cb fallbackUri: forward:/fallback/orders

كتابة GatewayFilter مخصص

عندما تكون الفلاتر المدمجة غير كافية يمكنك كتابة الخاص بك. نفّذ GatewayFilterFactory وسجّله كحبة Spring:

package com.example.gateway.filter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @Component public class ApiKeyGatewayFilterFactory extends AbstractGatewayFilterFactory<ApiKeyGatewayFilterFactory.Config> { public ApiKeyGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String apiKey = exchange.getRequest().getHeaders() .getFirst("X-Api-Key"); if (config.getRequiredKey().equals(apiKey)) { return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); }; } public static class Config { private String requiredKey; public String getRequiredKey() { return requiredKey; } public void setRequiredKey(String requiredKey) { this.requiredKey = requiredKey; } } }

استخدم المصنع بالاسم في YAML (اسم الفئة بدون لاحقة GatewayFilterFactory):

filters: - name: ApiKey args: requiredKey: ${INTERNAL_API_KEY}
اصطلاح تسمية الحبة مهم وظيفياً. يكتشف Spring Cloud Gateway حبوب GatewayFilterFactory بحسب اسم الفئة. يجب أن يتطابق مفتاح YAML بالضبط مع البادئة قبل GatewayFilterFactory (مع مراعاة حالة الأحرف). خطأ في هذا يُنتج استثناء FilterDefinitionNotFoundException عند بدء التشغيل.

مسارات Java DSL

YAML مريح للمسارات البسيطة، لكن Java DSL يمنحك دعم IDE الكامل والأمان في أنواع البيانات والمنطق البرمجي (مثل قراءة المسارات من قاعدة بيانات):

package com.example.gateway.config; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class GatewayConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("order-service", r -> r .path("/api/orders/**") .filters(f -> f .stripPrefix(1) .addRequestHeader("X-Gateway-Source", "my-gateway") .circuitBreaker(c -> c .setName("order-cb") .setFallbackUri("forward:/fallback/orders") ) ) .uri("lb://order-service") // lb:// = موزون التحميل عبر Eureka ) .build(); } }

يُخبر نظام URI ذو البادئة lb://order-service البوابةَ بحل اسم الخدمة عبر سجل Eureka وتوزيع التحميل عبر مثيلاتها باستخدام Spring Cloud LoadBalancer — الآلية نفسها التي تناولناها في الدرس الثالث.

التداعيات الأمنية

  • لا تثق أبداً بالرؤوس التي يُرسلها المُستدعون. إذا اعتمدت الخدمات الخلفية على الرؤوس X-User-Id أو X-Roles التي تُمرّرها البوابة، فاحذف هذه الرؤوس من الطلب الوارد أولاً ثم أضف رؤوسك الخاصة بعد المصادقة. وإلا يستطيع أي عميل تزويرها.
  • البوابة ليست جداراً نارياً. تأكد من أن الخدمات الخلفية غير قابلة للوصول مباشرةً من خارج شبكتك الخاصة. البوابة هي نقطة الدخول الوحيدة بالسياسة وبطبولوجيا الشبكة — لا بالاتفاقية وحسب.
  • سجّل معرّفات الارتباط. أضف GlobalFilter يُولّد UUID لكل طلب، يُرفق به كرأس (X-Correlation-Id)، ويُمرّره إلى الخدمات الخلفية. هذا يُتيح التتبع الموزع عبر خدمات متعددة.

الخلاصة

يُضبط Spring Cloud Gateway حول المسارات (routes)، تُطابق كل منها الطلبات الواردة عبر شروط (predicates) قابلة للتركيب وتُحوّلها عبر فلاتر (filters). تغطي الشروط المسار والأسلوب والرأس والاستعلام والوقت وتقسيم حركة المرور بالوزن. تتعامل الفلاتر المدمجة مع إعادة كتابة المسارات وحقن الرؤوس وتحديد معدل الطلبات وقطع الدائرة؛ أما حبوب GatewayFilterFactory المخصصة فتُتيح إضافة أي منطق عابر للخدمات تحتاجه. استخدم نظام URI ذا البادئة lb:// للتكامل السلس مع الخدمات المسجلة في Eureka. في الدرس القادم سترى كيف تُطبَّق المصادقة وCORS والتسجيل وتحديد معدل الطلبات فوق البوابة كاهتمامات عابرة للخدمات.