انتقل إلى المحتوى الرئيسي

الفصل 4: بوابة API مع Spring Cloud Gateway

🎯 أهداف الفصل​

  • فهم دور بوابة API
  • تكوين Spring Cloud Gateway
  • تعريف المسارات و الشروط
  • تنفيذ الفلاتر (قبل/بعد)
  • تكوين تحديد المعدل
  • التكامل مع Eureka لتحميل التوازن

1. لماذا بوابة API؟​

1.1 بدون بوابة​

المشاكل:

  • العميل يجب أن يعرف جميع الخدمات
  • CORS يجب تكوينه على كل خدمة
  • تكرار المصادقة في كل مكان
  • لا يوجد تحديد معدل مركزي
  • تسجيل متفرق
  • SSL يجب إدارته في كل مكان

1.2 مع بوابة​

المزايا:

  • نقطة دخول واحدة
  • مصادقة مركزية
  • تحديد معدل
  • تسجيل/مراقبة
  • إنهاء SSL
  • تحميل التوازن
  • تحويل الطلبات

2. Spring Cloud Gateway مقابل Zuul​

المعيارSpring Cloud GatewayZuul 1.x
الهيكليةتفاعلية (WebFlux)حجب (Servlet)
الأداءممتازجيد
البرمجةوظيفيةحتمية
الصيانةنشطةمهملة
Spring Boot 3✅ مدعوم❌ غير مدعوم

توصية: استخدم Spring Cloud Gateway للمشاريع الجديدة.


3. المفاهيم الأساسية​

3.1 المسار​

المسار هو الوحدة الأساسية: يحدد أين يتم إرسال الطلب.

spring:
cloud:
gateway:
routes:
- id: user-service-route
uri: http://localhost:8081
predicates:
- Path=/api/users/**

3.2 الشرط​

الشرط هو شرط لمطابقة الطلب.

predicates:
- Path=/api/users/** # حسب المسار
- Method=GET,POST # حسب طريقة HTTP
- Header=X-Request-Id, \d+ # حسب الهيدر
- Query=name # حسب بارامتر الاستعلام
- Host=**.example.com # حسب اسم المضيف
- After=2024-01-01T00:00:00 # حسب التاريخ

3.3 الفلتر​

الفلتر يعدل الطلب أو الاستجابة.

filters:
- AddRequestHeader=X-Gateway, true
- AddResponseHeader=X-Response-Time, 123
- RewritePath=/api/(?<segment>.*), /$\{segment}
- CircuitBreaker=name=myCircuitBreaker

3.4 تدفق المعالجة​


4. تكوين البوابة​

4.1 هيكل المشروع​

api-gateway/
├── pom.xml
└── src/main/
├── java/com/example/gateway/
│ ├── ApiGatewayApplication.java
│ ├── config/
│ │ ├── GatewayConfig.java
│ │ └── SecurityConfig.java
│ └── filter/
│ └── LoggingFilter.java
└── resources/
└── application.yml

4.2 تبعيات Maven​

<?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.2.0</version>
</parent>

<groupId>com.example</groupId>
<artifactId>api-gateway</artifactId>
<version>1.0.0</version>

<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>

<dependencies>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- Circuit Breaker -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

<!-- Rate Limiter (Redis) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</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>

4.3 التطبيق الرئيسي​

package com.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApiGatewayApplication {

public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}

5. تكوين YAML للمسارات​

5.1 مسارات أساسية​

server:
port: 8080

spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# مسار إلى خدمة المستخدم
- id: user-service
uri: http://localhost:8081
predicates:
- Path=/api/users/**

# مسار إلى خدمة المنتج
- id: product-service
uri: http://localhost:8082
predicates:
- Path=/api/products/**

# مسار إلى خدمة الطلب
- id: order-service
uri: http://localhost:8083
predicates:
- Path=/api/orders/**

# Eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

5.2 مسارات مع Eureka (تحميل التوازن)​

spring:
cloud:
gateway:
routes:
- id: user-service
# lb:// = تحميل متوازن عبر Eureka
uri: lb://user-service
predicates:
- Path=/api/users/**

- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**

- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**

# اكتشاف الخدمات تلقائيًا
discovery:
locator:
enabled: true
lower-case-service-id: true

5.3 مسارات مع الفلاتر​

spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
# إضافة هيدرز
- AddRequestHeader=X-Gateway-Source, api-gateway
- AddResponseHeader=X-Response-Time, ${timestamp}

# إعادة كتابة المسار
- RewritePath=/api/users/(?<segment>.*), /users/$\{segment}

# إزالة بادئة
- StripPrefix=1

# Circuit Breaker
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/users

6. الشروط المفصلة​

6.1 شرط المسار​

predicates:
# مسار دقيق
- Path=/api/users

# مع وايلدكارد بسيط (مقطع واحد)
- Path=/api/users/*

# مع وايلدكارد متعدد (عدة مقاطع)
- Path=/api/users/**

# عدة مسارات
- Path=/api/users/**,/api/accounts/**

6.2 شرط الطريقة​

predicates:
- Path=/api/users/**
- Method=GET,POST,PUT,DELETE

6.3 شرط الهيدر​

predicates:
- Path=/api/users/**
# هيدر موجود مع قيمة regex
- Header=X-Api-Key, ^[a-zA-Z0-9]{32}$
# هيدر موجود (بدون التحقق من القيمة)
- Header=Authorization

6.4 شرط الاستعلام​

predicates:
- Path=/api/search
# بارامتر الاستعلام موجود
- Query=q
# بارامتر الاستعلام مع قيمة regex
- Query=category, ^(electronics|books|clothing)$

6.5 شرط المضيف​

predicates:
- Host=**.example.com
- Host=api.example.com,admin.example.com

6.6 شروط التاريخ والوقت​

predicates:
# نشط بعد هذا التاريخ
- After=2024-01-01T00:00:00+00:00[UTC]

# نشط قبل هذا التاريخ
- Before=2025-01-01T00:00:00+00:00[UTC]

# نشط بين تاريخين
- Between=2024-01-01T00:00:00+00:00[UTC], 2024-12-31T23:59:59+00:00[UTC]

6.7 شرط الوزن (اختبار A/B)​

routes:
- id: user-service-v1
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
- Weight=group1, 8 # 80% من الحركة

- id: user-service-v2
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- Weight=group1, 2 # 20% من الحركة

7. الفلاتر المفصلة​

7.1 AddRequestHeader / AddResponseHeader​

filters:
- AddRequestHeader=X-Request-Source, gateway
- AddResponseHeader=X-Gateway-Version, 1.0

7.2 RemoveRequestHeader / RemoveResponseHeader​

filters:
- RemoveRequestHeader=Cookie
- RemoveResponseHeader=X-Powered-By

7.3 RewritePath​

filters:
# /api/users/123 → /users/123
- RewritePath=/api/(?<segment>.*), /$\{segment}

# /old-api/users → /api/v2/users
- RewritePath=/old-api/(?<segment>.*), /api/v2/$\{segment}

7.4 StripPrefix / PrefixPath​

filters:
# /api/v1/users → /users (إزالة مقطعين)
- StripPrefix=2

# /users → /api/users (إضافة بادئة)
- PrefixPath=/api

7.5 SetStatus​

filters:
# فرض كود استجابة
- SetStatus=401

7.6 Retry​

filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET
backoff:
firstBackoff: 100ms
maxBackoff: 500ms
factor: 2

7.7 RequestRateLimiter​

filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id") != null
? exchange.getRequest().getHeaders().getFirst("X-User-Id")
: exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}

8. التكوين البرمجي (Java)​

8.1 RouteLocator Bean​

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 customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// مسار خدمة المستخدم
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "true")
.circuitBreaker(c -> c
.setName("userServiceCB")
.setFallbackUri("forward:/fallback/users")
)
)
.uri("lb://user-service")
)

// مسار خدمة المنتج
.route("product-service", r -> r
.path("/api/products/**")
.and()
.method("GET", "POST", "PUT", "DELETE")
.filters(f -> f
.rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
.retry(config -> config
.setRetries(3)
.setStatuses(HttpStatus.BAD_GATEWAY)
)
)
.uri("lb://product-service")
)

// مسار مع تحديد المعدل
.route("order-service", r -> r
.path("/api/orders/**")
.filters(f -> f
.requestRateLimiter(config -> config
.setRateLimiter(redisRateLimiter())
.setKeyResolver(userKeyResolver())
)
)
.uri("lb://order-service")
)

.build();
}
}

9. الفلاتر المخصصة​

9.1 فلتر عالمي (جميع المسارات)​

package com.example.gateway.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

@Component
public class LoggingFilter implements GlobalFilter, Ordered {

private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// فلتر قبل: قبل الإرسال إلى الخدمة
String requestId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();

ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-Request-Id", requestId)
.build();

log.info(">>> Request: {} {} | RequestId: {}",
request.getMethod(),
request.getURI().getPath(),
requestId
);

// تعديل exchange مع الطلب الجديد
ServerWebExchange modifiedExchange = exchange.mutate()
.request(request)
.build();

return chain.filter(modifiedExchange)
.then(Mono.fromRunnable(() -> {
// فلتر بعد: بعد الاستجابة
long duration = System.currentTimeMillis() - startTime;
log.info("<<< Response: {} | Duration: {}ms | RequestId: {}",
exchange.getResponse().getStatusCode(),
duration,
requestId
);
}));
}

@Override
public int getOrder() {
return -1; // يتم تنفيذه أولاً
}
}

9.2 GatewayFilter (مسار محدد)​

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;

@Component
public class AuthenticationFilter extends AbstractGatewayFilterFactory<AuthenticationFilter.Config> {

public AuthenticationFilter() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String authHeader = exchange.getRequest()
.getHeaders()
.getFirst("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}

String token = authHeader.substring(7);

// التحقق من صحة الرمز (مبسط)
if (!isValidToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}

return chain.filter(exchange);
};
}

private boolean isValidToken(String token) {
// تنفيذ منطق التحقق من صحة JWT
return token != null && token.length() > 10;
}

public static class Config {
// تكوين الفلتر إذا لزم الأمر
}
}

الاستخدام في YAML:

filters:
- Authentication

10. Fallback و Circuit Breaker​

10.1 تكوين Fallback​

spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/users

10.2 وحدة تحكم Fallback​

package com.example.gateway.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/fallback")
public class FallbackController {

@GetMapping("/users")
public ResponseEntity<Map<String, String>> userServiceFallback() {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"message", "خدمة المستخدم غير متاحة حاليًا",
"status", "FALLBACK"
));
}

@GetMapping("/products")
public ResponseEntity<Map<String, String>> productServiceFallback() {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"message", "خدمة المنتج غير متاحة حاليًا",
"status", "FALLBACK"
));
}

@GetMapping("/orders")
public ResponseEntity<Map<String, String>> orderServiceFallback() {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"message", "خدمة الطلب غير متاحة حاليًا",
"status", "FALLBACK"
));
}
}

10.3 تكوين Resilience4j​

resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
instances:
userServiceCB:
baseConfig: default
productServiceCB:
baseConfig: default
failureRateThreshold: 30

11. تكوين CORS​

package com.example.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@Configuration
public class CorsConfig {

@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(List.of(
"http://localhost:3000",
"https://myapp.com"
));
corsConfig.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
corsConfig.setAllowedHeaders(List.of("*"));
corsConfig.setAllowCredentials(true);
corsConfig.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);

return new CorsWebFilter(source);
}
}

أو عبر YAML:

spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "http://localhost:3000"
allowedMethods:
- GET
- POST
- PUT
- DELETE
allowedHeaders: "*"
allowCredentials: true

12. المراقبة و Actuator​

management:
endpoints:
web:
exposure:
include: health,info,gateway
endpoint:
gateway:
enabled: true

نقاط النهاية المفيدة​

# قائمة المسارات
curl http://localhost:8080/actuator/gateway/routes

# تحديث المسارات
curl -X POST http://localhost:8080/actuator/gateway/refresh

# الفلاتر العالمية
curl http://localhost:8080/actuator/gateway/globalfilters

# فلاتر المسار
curl http://localhost:8080/actuator/gateway/routefilters

13. ملخص​

النقاط الرئيسية​

✅ بوابة API = نقطة دخول واحدة
✅ المسارات = تحدد أين يتم إرسال الطلبات
✅ الشروط = شروط المطابقة
✅ الفلاتر = تعديل الطلب/الاستجابة
✅ lb:// = تحميل التوازن عبر Eureka
✅ CircuitBreaker = حماية من الأعطال
✅ RateLimiter = حماية من الإساءة

تكوين نوع الإنتاج​

spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/users
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200

الفصل التالي: الاتصال بين الخدمات →

العودة إلى الوحدة 13