Backend

99 Ngày Spring — Ngày 74: API Gateway

SSite Admin
9 tháng 10, 2026 7 phút đọc 6 lượt xem
99 Ngày Spring — Ngày 74: API Gateway

Ngày 73 các service đã tìm thấy nhau qua danh bạ. Nhưng ứng dụng mobile và web không nên biết có bao nhiêu service, chúng tên gì và chạy ở đâu; chúng cũng không nên phải tự lo xác thực với từng service. API Gateway là cửa vào duy nhất: nhận mọi request, định tuyến theo đường dẫn tới đúng service, và làm những việc chung như xác thực, giới hạn tốc độ, gắn id truy vết. Bài này dùng Spring Cloud Gateway trên Spring Boot 3.5, Spring Cloud 2025.0 và Java 21.

API Gateway: route và predicate, filter, xác thực JWT tại cửa, timeout và fallback

1. Gateway làm gì, và không làm gì

  • Định tuyến: /api/orders/** đi tới order-service, /api/payments/** tới payment-service; client chỉ biết một hostname.

  • Việc chung: xác thực token, CORS, TLS termination, rate limiting, correlation id, nén, cache header. Làm một lần ở cửa thay vì lặp trong mỗi service.

  • Che topology: đổi tên service, tách một service làm hai, chuyển dần sang phiên bản mới: client không cần biết.

  • Không phải nơi đặt nghiệp vụ. Gateway ghép dữ liệu từ ba service để trả một màn hình là dấu hiệu bạn đang xây một service ẩn không có chủ. Nếu cần ghép, đó là một service tên là BFF (backend for frontend), có test và có người chịu trách nhiệm.

2. Route, predicate, filter

Spring Cloud Gateway chạy trên WebFlux (Netty), nên dự án gateway không thêm spring-boot-starter-web: có cả hai stack trên classpath là gateway không khởi động. Dependency: spring-cloud-starter-gateway-server-webflux (tên từ Spring Cloud 2025.0; bản trước là spring-cloud-starter-gateway), cộng spring-cloud-starter-netflix-eureka-client và spring-cloud-starter-loadbalancer để hiểu lb://.

# gateway/src/main/resources/application.yml
# Spring Cloud 2025.0 (Gateway 5): tiền tố spring.cloud.gateway.server.webflux.*
# (bản trước là spring.cloud.gateway.* — vẫn chạy nhưng đã deprecated)
server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: orders
              uri: lb://order-service            # lb:// = tra danh bạ (Ngày 73) rồi cân bằng tải
              predicates:
                - Path=/api/orders/**
              filters:
                - StripPrefix=1                    # /api/orders/42 → /orders/42 ở downstream
            - id: payments
              uri: lb://payment-service
              predicates:
                - Path=/api/payments/**
                - Method=GET,POST
              filters:
                - StripPrefix=1
                - AddRequestHeader=X-Gateway, motdev
                - name: CircuitBreaker             # Resilience4j — chi tiết Ngày 76
                  args:
                    name: payments
                    fallbackUri: forward:/fallback/payments
          httpclient:
            connect-timeout: 2000                  # ms
            response-timeout: 5s
management:
  endpoints:
    web:
      exposure:
        include: health,gateway                  # GET /actuator/gateway/routes để soi route đã nạp
  • Mỗi route có id, uri đích, danh sách predicate (điều kiện khớp: Path, Method, Header, Host, Query, thời gian…) và danh sách filter biến đổi request/response. Route được xét theo thứ tự; route khớp đầu tiên thắng, nên đặt route cụ thể trước route rộng.

  • lb://order-service tra danh bạ và cân bằng tải; http://localhost:8081 trỏ thẳng khi chưa có discovery. StripPrefix=1 bỏ /api để downstream giữ đường dẫn tự nhiên của nó.

  • Có thể khai báo route bằng Java (RouteLocatorBuilder) khi cần logic; YAML đủ cho phần lớn trường hợp và dễ đưa vào Config Server (Ngày 72) để đổi route không cần build lại.

  • Route thường được thêm dần: /actuator/gateway/routes là nơi kiểm tra gateway thật sự đã nạp những gì.

# Route nào đã nạp, thứ tự và filter của nó:
curl -s localhost:8080/actuator/gateway/routes | jq '.[] | {route_id, predicate, filters}'

# Gọi thử qua gateway (token lấy từ auth server):
curl -i -H "Authorization: Bearer $TOKEN" localhost:8080/api/orders/42
# HTTP/1.1 200
# X-Request-Id: 3f1c...   ← do CorrelationIdFilter gắn, cùng id xuất hiện trong log của order-service

3. Filter: từng route hoặc toàn cục

package vn.motdev.gateway;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

// Global filter: áp cho MỌI route. Gateway là WebFlux nên filter là reactive — không chặn luồng.
@Component
public class CorrelationIdFilter implements GlobalFilter, Ordered {
    static final String HEADER = "X-Request-Id";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String id = exchange.getRequest().getHeaders().getFirst(HEADER);
        String requestId = (id == null || id.isBlank()) ? UUID.randomUUID().toString() : id;
        var mutated = exchange.mutate()
                .request(r -> r.headers(h -> h.set(HEADER, requestId)))   // gửi xuống downstream
                .build();
        mutated.getResponse().getHeaders().set(HEADER, requestId);        // và trả lại cho client
        return chain.filter(mutated);
    }

    @Override
    public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }   // chạy trước mọi filter khác
}
  • Filter theo route (AddRequestHeader, RewritePath, Retry, RequestRateLimiter, CircuitBreaker…) khai báo trong YAML. GlobalFilter áp cho mọi route, thứ tự theo Ordered.

  • Correlation id gắn ở gateway rồi truyền xuống là nền của distributed tracing (Ngày 77): mọi log của một request, qua ba service, chia sẻ cùng một id. Micrometer Tracing sẽ làm việc này tự động bằng header chuẩn traceparent; filter trên là phiên bản tối giản để hiểu cơ chế.

  • Filter là reactive: không gọi block(), không gọi JDBC, không gọi RestTemplate bên trong. Cần gọi ra ngoài (kiểm tra token với auth server, đọc quota) thì dùng WebClient và trả Mono.

  • RequestRateLimiter mặc định dựa trên Redis (token bucket) với KeyResolver chọn khóa: theo user, theo API key hay theo IP. Rate limit theo IP phía sau proxy cần X-Forwarded-For được tin cậy đúng cách, nếu không mọi client chung một IP.

4. Xác thực tại cửa

package vn.motdev.gateway;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

// dependency: spring-boot-starter-oauth2-resource-server (bản WebFlux được chọn tự động trong gateway)
// spring.security.oauth2.resourceserver.jwt.issuer-uri: https://auth.motdev.vn/realms/shop
@Configuration
@EnableWebFluxSecurity
public class GatewaySecurity {
    @Bean
    SecurityWebFilterChain chain(ServerHttpSecurity http) {
        return http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)        // API stateless, xác thực bằng Bearer
                .authorizeExchange(ex -> ex
                        .pathMatchers("/actuator/health").permitAll()
                        .pathMatchers("/api/payments/**").hasAuthority("SCOPE_payments")
                        .anyExchange().authenticated())
                .oauth2ResourceServer(rs -> rs.jwt(org.springframework.security.config.Customizer.withDefaults()))
                .build();
    }
}
// Token hợp lệ đi tiếp; filter TokenRelay (spring-cloud-starter-gateway-server-webflux + oauth2-client)
// chuyển Bearer xuống downstream để service vẫn biết "ai" đang gọi.
  • Gateway là resource server: kiểm chữ ký và hạn của JWT bằng khóa công khai từ issuer-uri, không cần gọi auth server cho mỗi request. Token hỏng bị chặn ở cửa với 401; thiếu scope là 403; downstream không bao giờ thấy request rác.

  • Chuyển tiếp danh tính: filter TokenRelay gửi nguyên Bearer xuống downstream để service biết ai gọi và tự phân quyền chi tiết (@PreAuthorize). Đây là defense in depth: gateway lọc thô, service quyết định theo nghiệp vụ. Bỏ kiểm tra ở service vì “gateway đã kiểm” là đánh cược rằng không ai gọi thẳng vào mạng nội bộ.

  • Với client không giữ được secret (SPA, mobile), gateway có thể đóng vai OAuth2 client (spring-boot-starter-oauth2-client), giữ phiên và token ở phía server, phát cookie cho trình duyệt: mẫu BFF cho xác thực.

  • CSRF tắt vì API stateless dùng Bearer; nếu gateway phát cookie phiên cho SPA thì phải bật lại.

5. Timeout, fallback và những lỗi vận hành hay gặp

  • Đặt connect-timeout và response-timeout cho HTTP client của gateway; không có timeout thì một downstream treo giữ kết nối của gateway cho tới khi cạn. Timeout theo route có thể ghi đè qua metadata của route.

  • Filter CircuitBreaker với fallbackUri trả câu trả lời dự phòng khi downstream lỗi liên tục; chi tiết trạng thái, ngưỡng và bulkhead ở Ngày 76.

  • Gateway là điểm nghẽn có chủ đích: chạy ít nhất hai instance sau một load balancer L4/L7 hoặc Ingress; gateway đơn lẻ là single point of failure của cả hệ thống.

  • Đội chỉ dùng servlet stack có Spring Cloud Gateway Server MVC (spring-cloud-starter-gateway-server-webmvc): cùng khái niệm route/predicate/filter trên Spring MVC, không cần học reactive, đổi lại throughput với nhiều kết nối chờ thấp hơn bản WebFlux. Với Java 21, virtual thread thu hẹp khoảng cách đó đáng kể.

6. Bài tập thực hành

  • Dựng gateway với hai route trỏ lb:// tới hai service của Ngày 73; gọi /actuator/gateway/routes và đọc lại thứ tự predicate.

  • Thêm CorrelationIdFilter, log header X-Request-Id ở downstream và xác nhận cùng một id đi xuyên suốt; thử gửi id từ client và xác nhận gateway giữ nguyên.

  • Bật resource server với một issuer thật (Keycloak local hoặc auth server của Ngày 45); gọi không token, token hết hạn, token thiếu scope và ghi lại ba mã trạng thái.

  • Cho payment-service ngủ 10 giây; đặt response-timeout: 2s và quan sát gateway trả 504 thay vì treo. Thêm fallbackUri và so sánh trải nghiệm.

Tóm lại: gateway là cửa vào duy nhất định tuyến bằng route, predicate và filter; việc chung như correlation id, rate limit và xác thực JWT làm tại cửa rồi chuyển tiếp danh tính xuống dưới; timeout và fallback bắt buộc để downstream lỗi không kéo sập cửa; và gateway không phải nơi đặt nghiệp vụ. Ngày 75 quay vào bên trong: các service gọi nhau thế nào cho gọn với @LoadBalanced RestClient, HTTP interface, OpenFeign, và retry đúng cách.

Tài liệu đối chiếu

Spring Cloud Gateway reference: https://docs.spring.io/spring-cloud-gateway/reference/ ; Spring Security OAuth2 Resource Server (WebFlux): https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/index.html ; Gateway Server MVC: https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-webmvc.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 74: JIT & tối ưu runtime

Interpreter rồi C1 và C2, warmup và đo bằng JMH, inlining và escape analysis, deoptimization khi JIT đoán sai, code cache, CDS và AOT cho khởi động nhanh.

9 thg 10, 20269 phút5
99 Ngày Java — Ngày 73: ClassLoader

Ba giai đoạn nạp, link, init; ba loader và mô hình ủy quyền cha trước; custom loader và định danh lớp theo loader; phân biệt ClassNotFoundException với NoClassDefFoundError.

8 thg 10, 20269 phút8
99 Ngày Spring — Ngày 73: Service discovery với Eureka

Đăng ký, heartbeat và eviction, cache danh bạ phía client, Eureka server và client trên Spring Boot 3.5, gọi bằng tên với @LoadBalanced RestClient, và khi nào Kubernetes làm Eureka thừa.

8 thg 10, 20266 phút8