Spring Cloud Gateway: Unified Entry Point for Microservice Calls
This tutorial walks through adding Spring Cloud Gateway as a single entry point for client calls to multiple microservices, covering why a gateway is needed, project structure, dependency and configuration setup, global logging and authentication filters, startup verification, and common pitfalls with solutions.
Goal
┌─────────────────────────────────────────────────────────────┐
│ Final Effect of This Episode │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client ──► Gateway(:8080) ──┬──► user-service(:8081) │
│ │ │ │
│ └──► order-service(:8082) │
│ │
│ ✅ Unified entry: all requests go through the gateway │
│ ✅ Route forwarding based on path │
│ ✅ Centralized authentication │
│ ✅ Global CORS configuration │
└─────────────────────────────────────────────────────────────┘Why a Gateway?
Problems without a gateway
Clients must remember dozens of service URLs.
Each service needs its own CORS configuration.
Login verification is duplicated in every service.
Changing a service address requires updating all clients.
Architecture after adding a gateway
┌─────────────────────────────────────┐
│ Gateway (:8080) │
│ http://api.teaching.com │
└─────────────────┬───────────────────┘
│
┌─────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User svc │ │ Order svc│ │ Product │
│ :8081 │ │ :8082 │ │ :8083 │
└──────────┘ └──────────┘ └──────────┘Advantages
Clients interact only with the gateway.
Unified authentication, logging and rate limiting.
Service address changes are transparent to clients.
Project Structure (new gateway module)
spring-cloud-teaching-ep04/
├── pom.xml # parent project, adds gateway dependency management
├── docker-compose.yml
├── user-service/ # same as previous episodes
│ └── ...
├── order-service/ # same as previous episodes
│ └── ...
├── gateway/ # ★ added in this episode
│ ├── pom.xml
│ └── src/main/java/com/teaching/gateway/
│ ├── GatewayApplication.java
│ ├── config/
│ │ └── GatewayConfig.java # route configuration
│ └── filter/
│ ├── AuthGlobalFilter.java # global auth filter
│ └── LogGlobalFilter.java # logging filter
└── README.mdGateway Module Details
1. Dependency Configuration (gateway/pom.xml)
<!-- gateway/pom.xml -->
<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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.teaching</groupId>
<artifactId>spring-cloud-teaching</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>gateway</artifactId>
<dependencies>
<!-- ★ Gateway dependency (do NOT add spring-boot-starter-web) -->
<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>
<!-- Load balancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>2. Configuration File (gateway/src/main/resources/application.yml)
# gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: user-service-route
uri: lb://user-service # lb:// means load‑balanced
predicates:
- Path=/api/user/**
filters:
- StripPrefix=0
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=0
httpclient:
connect-timeout: 3000
response-timeout: 30s
logging:
level:
org.springframework.cloud.gateway: DEBUG3. Startup Class
// gateway/src/main/java/com/teaching/gateway/GatewayApplication.java
package com.teaching.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
System.out.println("✅ Gateway started successfully! Port: 8080");
}
}4. Global Logging Filter
// gateway/src/main/java/com/teaching/gateway/filter/LogGlobalFilter.java
package com.teaching.gateway.filter;
import lombok.extern.slf4j.Slf4j;
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;
@Component
@Slf4j
public class LogGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long startTime = System.currentTimeMillis();
String path = exchange.getRequest().getURI().getPath();
String method = exchange.getRequest().getMethod().name();
log.info("Request incoming: {} {}", method, path);
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
int status = exchange.getResponse().getStatusCode() != null ?
exchange.getResponse().getStatusCode().value() : 0;
log.info("Request completed: {} {} - {} ({}ms)", method, path, status, duration);
}));
}
@Override
public int getOrder() {
return -1; // high priority
}
}5. Global Authentication Filter (example)
// gateway/src/main/java/com/teaching/gateway/filter/AuthGlobalFilter.java
package com.teaching.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final String[] WHITE_LIST = {
"/api/user/login",
"/api/user/register",
"/actuator/health"
};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 1. White‑list pass‑through
if (isWhiteList(path)) {
return chain.filter(exchange);
}
// 2. Get Token
String token = getToken(request);
if (!StringUtils.hasText(token)) {
return unauthorized(exchange, "Missing Token");
}
// 3. Token validation (simplified)
if (!validateToken(token)) {
return unauthorized(exchange, "Invalid or expired Token");
}
// 4. Extract user info and inject into header
String userId = extractUserId(token);
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private boolean isWhiteList(String path) {
for (String white : WHITE_LIST) {
if (path.startsWith(white)) {
return true;
}
}
return false;
}
private String getToken(ServerHttpRequest request) {
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private boolean validateToken(String token) {
// Simplified: token must start with "valid_"
return token.startsWith("valid_");
}
private String extractUserId(String token) {
// Simplified: always return "1"
return "1";
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
String body = String.format("{\"code\":401,\"message\":\"%s\"}", 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, before logging filter
}
}Startup Verification
1. Start all services
# 1. Start Nacos
docker-compose up -d
# 2. Start user-service
cd user-service
mvn spring-boot:run
# 3. Start order-service
cd ../order-service
mvn spring-boot:run
# 4. Start gateway (new terminal)
cd ../gateway
mvn spring-boot:run2. Verify routing
# Call user-service via gateway
curl http://localhost:8080/api/user/1
# Call order-service (which may call user-service) via gateway
curl http://localhost:8080/api/order/13. Verify Nacos registration
Visit http://localhost:8848/nacos; the service list should contain:
user-service order-service gatewayCommon Issues & Pitfalls
Pitfall 1: Gateway and Web dependency conflict
Symptom: Startup error "Spring MVC found on classpath".
Cause: Gateway is based on WebFlux and cannot coexist with spring-boot-starter-web.
Solution: Ensure the gateway module does not include the web starter.
Pitfall 2: Routes not taking effect
Debug steps:
Check that the predicates path patterns are correct.
Confirm the services are registered in Nacos.
Enable Gateway debug logs by setting org.springframework.cloud.gateway: TRACE in logging.level.
Pitfall 3: lb:// service name not found
Cause: Missing load‑balancer dependency or mismatched service name.
Solution: Add the spring-cloud-starter-loadbalancer dependency (as shown in the pom snippet) and ensure the service name matches the registration.
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.
