How to Expose Spring Cloud Microservices to Frontends with Spring Cloud Gateway
This article explains why a gateway is needed in a Spring Cloud microservice architecture, compares Spring Cloud Gateway with Zuul, introduces its core concepts of routes, predicates, and filters, and provides step‑by‑step code examples for dependency setup, configuration, custom authentication and logging filters, rate limiting, circuit breaking, troubleshooting tips, and a complete gateway configuration for a blog system.
Why a Gateway?
Without a gateway, clients must remember dozens of service URLs, configure CORS for each service, duplicate authentication logic, and any service address change forces client updates.
Gateway Architecture
Spring Cloud Gateway acts as the "big door" of the microservice system, providing a single entry point that forwards requests to backend services while handling unified authentication, logging, and rate limiting, and making service address changes transparent to clients.
Comparison with Zuul
Underlying platform: Zuul 1.x uses Servlet, Zuul 2.x uses Netty, Gateway uses WebFlux.
Blocking vs non‑blocking: Zuul 1.x is blocking, Zuul 2.x is partially non‑blocking, Gateway is fully non‑blocking.
Performance: low for Zuul 1.x, medium for Zuul 2.x, high for Gateway.
Maintenance: Zuul 1.x is discontinued, Zuul 2.x is inactive, Gateway is actively maintained.
Spring Cloud integration: good for Zuul 1.x, average for Zuul 2.x, native for Gateway.
Core Concepts
Gateway consists of three building blocks:
Route : defines the target service and path.
Predicate : evaluates conditions (e.g., path, method) to decide whether the route matches.
Filter : performs pre‑ and post‑processing such as header manipulation, logging, or retries.
Quick Start
1. Add Dependencies
<!-- gateway service pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos service discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Note: do NOT add spring-boot-starter-web, it conflicts with Gateway -->2. Basic Configuration
# gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=0
- id: article-service-route
uri: lb://article-service
predicates:
- Path=/api/article/**
filters:
- StripPrefix=0
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true3. Verify
# Start Nacos
docker-compose up -d
# Start services
java -jar user-service.jar
java -jar article-service.jar
java -jar gateway.jar
# Test via gateway
curl http://localhost:8080/api/user/1 # → forwards to user-service:8081/api/user/1Predicate Factory
Path – path matching (e.g., - Path=/api/user/**)
Method – HTTP method filter (e.g., - Method=GET,POST)
Header – request header pattern (e.g., - Header=X-API-Version, v\d+)
Query – query parameter pattern (e.g., - Query=page, \d+)
Cookie – cookie pattern (e.g., - Cookie=user-token, .+)
Host – host name pattern (e.g., - Host=**.blog.com)
RemoteAddr – client IP range (e.g., - RemoteAddr=192.168.0.0/24)
Before/After – time window (e.g., - Before=2025-12-31T23:59:59+08:00)
Filters
Built‑in Filters
AddRequestHeader / AddResponseHeader
AddRequestParameter
StripPrefix
RewritePath
Retry (e.g., 3 retries on INTERNAL_SERVER_ERROR)
RequestRateLimiter (Redis‑based rate limiting)
Custom Global Filter – Unified Authentication
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtils jwtUtils;
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/user/login",
"/api/user/register",
"/api/article/public/**",
"/actuator/**"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 1. White‑list pass
if (isWhiteList(path)) {
return chain.filter(exchange);
}
// 2. Get token
String token = getToken(request);
if (StringUtils.isBlank(token)) {
return unauthorized(exchange, "Missing Token");
}
// 3. Validate token
if (!jwtUtils.validateToken(token)) {
return unauthorized(exchange, "Invalid or expired token");
}
// 4. Extract user info and inject headers
Long userId = jwtUtils.getUserIdFromToken(token);
String username = jwtUtils.getUsernameFromToken(token);
ServerHttpRequest newRequest = request.mutate()
.header("X-User-Id", String.valueOf(userId))
.header("X-Username", username)
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
}
private boolean isWhiteList(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
private String getToken(ServerHttpRequest request) {
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = JSON.toJSONString(Result.error(401, message));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() { return -100; } // highest priority
}Custom Local Filter – Request Logging
@Component
@Slf4j
public class RequestLogGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
long startTime = System.currentTimeMillis();
log.info("Request: {} {} from {}", request.getMethod(), request.getURI(), request.getRemoteAddress());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
long duration = System.currentTimeMillis() - startTime;
log.info("Response: {} {} - {} ({}ms)", request.getMethod(), request.getURI(), response.getStatusCode(), duration);
}));
}
@Override
public int getOrder() { return 0; }
}Rate Limiting
Define a RedisRateLimiter bean (10 requests per second, burst capacity 20) and three KeyResolver s for IP, user ID, and API path. Apply the filter in application.yml with name: RequestRateLimiter and the chosen resolver.
Circuit Breaker
Include the Resilience4j starter, configure a circuit breaker named userServiceBreaker (slidingWindowSize 10, failureRateThreshold 50 %, waitDurationInOpenState 30 s, permittedNumberOfCallsInHalfOpenState 5). Add a CircuitBreaker filter to the user‑service route and a fallback controller that returns a 503 JSON response.
Full Configuration Example
# gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:8848
namespace: blog-dev
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**,/api/auth/**
filters:
- StripPrefix=0
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 40
- id: article-service
uri: lb://article-service
predicates:
- Path=/api/article/**,/api/category/**
filters:
- StripPrefix=0
- name: CircuitBreaker
args:
name: articleServiceBreaker
fallbackUri: forward:/fallback/article
- id: comment-service
uri: lb://comment-service
predicates:
- Path=/api/comment/**
filters:
- StripPrefix=0
- id: search-service
uri: lb://search-service
predicates:
- Path=/api/search/**
filters:
- StripPrefix=0
httpclient:
connect-timeout: 3000
response-timeout: 30s
redis:
host: localhost
port: 6379
logging:
level:
org.springframework.cloud.gateway: DEBUGCommon Issues & Troubleshooting
1. Dependency Conflict
Gateway runs on WebFlux; having spring-boot-starter-web on the classpath causes a startup error ("Spring MVC found on classpath"). Remove the web starter and keep only spring-boot-starter-webflux.
2. Route Not Effective
When a request returns 404, check that the predicates paths are correct, ensure the target service is registered in Nacos, and enable TRACE logging for org.springframework.cloud.gateway to see routing decisions.
3. WebSocket Support
Add a route with Path=/ws/** and the appropriate uri (e.g., lb://websocket-service) to enable WebSocket proxying.
Next Episode Preview
Spring Cloud Microservice Practice (5): Nacos Configuration Center – unified configuration management, dynamic refresh, multi‑environment isolation, and version rollback.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
