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.

1. Gateway làm gì, và không làm gì
Định tuyến:
/api/orders/**đi tớiorder-service,/api/payments/**tớipayment-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ạpMỗ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-servicetra danh bạ và cân bằng tải;http://localhost:8081trỏ thẳng khi chưa có discovery.StripPrefix=1bỏ/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/routeslà 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-service3. 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ự theoOrdered.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ọiRestTemplatebên trong. Cần gọi ra ngoài (kiểm tra token với auth server, đọc quota) thì dùngWebClientvà trảMono.RequestRateLimitermặc định dựa trên Redis (token bucket) vớiKeyResolverchọn khóa: theo user, theo API key hay theo IP. Rate limit theo IP phía sau proxy cầnX-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
TokenRelaygử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-timeoutvàresponse-timeoutcho 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
CircuitBreakervớifallbackUritrả 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/routesvà đọc lại thứ tự predicate.Thêm
CorrelationIdFilter, log headerX-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-servicengủ 10 giây; đặtresponse-timeout: 2svà quan sát gateway trả 504 thay vì treo. ThêmfallbackUrivà 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
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.


