In this story, we’ll explore how to implement API security with an API key using Spring Cloud Gateway and Redis.
· Prerequisites
· Overview
∘ Why Use API Keys with Spring Cloud Gateway?
∘ Architecture Overview
∘ Setting up a Redis Instance
· Setting Up Spring Cloud Gateway
∘ Implementation Strategy
∘ Building the API Key Initializer
∘ Creating the API Key Filter
∘ Configuring Routes (Spring Cloud Gateway 4.x)
· Microservices
∘ Microservice A
∘ Microservice B
· Testing
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites:
- Docker / Docker-compose installed (optional if you’ve already downloaded and installed Redis)
- Spring Boot 4
- Maven 3.6.3 or later
- Java 21 or higher
- IntelliJ IDEA, Visual Studio Code, or another IDE
- Postman / insomnia or any other API testing tool.
Overview
In microservices architectures, the security of data passing between the different API services becomes decisive. It is therefore essential to establish multiple levels to enhance security between the various exchanges of these services.
That’s why the API Security Model, based on Richardson Maturity Model, describes API security at increasing levels of security.

In this model, security and trust are increasingly improved at each level. It has four levels:
- Level 0: API Keys and Basic Authentication
- Level 1: Token-Based Authentication
- Level 2: Token-Based Authorization
- Level 3: Centralized Trust Using Claims
In this story, we’ll focus on level 0 (API Keys) with implementation through the Spring Cloud Gateway.
Why Use API Keys with Spring Cloud Gateway?
API keys offer a simple yet effective authentication mechanism for:
- Internal services communication
- Third-party API access control
- Monitoring and rate limiting
- Service-specific access permissions
Architecture Overview
Spring Cloud Gateway (SCG) is a Spring-based API Gateway framework that provides a centralized entry point for routing, filtering, and securing requests to backend services in a microservices architecture.
It supports two execution models (especially relevant for Spring Boot 3.x and toward 4.x.):
- Reactive (WebFlux) for non-blocking, high-throughput gateways
- Servlet-based (Web MVC) for blocking, traditional Spring MVC environments
Because it sits in front of all microservices, it is the ideal place to validate API keys once, before traffic reaches backend services.
Here is the sequence diagram:

A client sends an HTTP request to the API Gateway with an API key in the header to access the /payments/checkout endpoint.
The gateway extracts the API key and service name, then validates the key by looking it up in Redis.
- If the API key is not found, the gateway returns 401 Unauthorized.
- If found, the gateway checks that the key is active, not expired, and authorized for the requested service.
- If any check fails, it returns 403 Forbidden.
- If all checks pass, the gateway forwards the request to the appropriate microservice, rewriting the path and adding a header to indicate successful authentication.
The microservice processes the request and returns a response, which the gateway then forwards back to the client.
Setting up a Redis Instance
This story uses the Docker approach to create a Redis instance. Here is the full content of the docker-compose file:
version: '3'
services:
redis:
image: redis:latest
container_name: redis_db
ports:
- "6379:6379"
volumes:
- ./data:/data
restart: always
Setting Up Spring Cloud Gateway
Creating a simple Spring Boot project from start.spring.io with the following dependencies: Lombok, Reactive Gateway, Spring Data Reactive Redis.
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Implementation Strategy
Our implementation will consist of three main components:
- API Key Initializer — a startup component that preloads API key data into Redis.
- Global Filter — Intercepts all requests to validate API keys
- Configuration — Routes and security settings
Building the API Key Initializer
The ApiKeyInitializer class is responsible for bootstrapping predefined API key data into Redis at application startup.
@Slf4j
@Component
public class ApiKeyInitializer {
private final ReactiveRedisTemplate<String, ApiKey> redisTemplate;
public ApiKeyInitializer(ReactiveRedisTemplate<String, ApiKey> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@PostConstruct
public void initData() {
List<ApiKey> apiKeys = List.of(
new ApiKey("343C-ED0B-4137-B27E",
List.of(
new AppService(CUSTOMERS_SERVICE_NAME, LocalDateTime.now().plusDays(30)),
new AppService(PAYMENTS_SERVICE_NAME, null)
)),
new ApiKey("FA48-EF0C-427E-8CCF",
List.of(
new AppService(CUSTOMERS_SERVICE_NAME, LocalDateTime.now().minusDays(25)),
new AppService(PAYMENTS_SERVICE_NAME, LocalDateTime.now().minusHours(3))
))
);
Flux.fromIterable(apiKeys)
.flatMap(apiKey -> redisTemplate.opsForValue().set(getKey(apiKey), apiKey))
.doOnComplete(() -> log.info("API keys initialized in Redis"))
.subscribe();
}
private static String getKey(ApiKey apiKey) {
return RECORD_KEY + apiKey.getKey();
}
It ensures that Redis contains initial API key data at startup so that the Spring Cloud Gateway can reliably authenticate and authorize requests using a reactive mechanism.
For production use, API keys should be created, updated, rotated, and revoked through API Key Management endpoints, replacing startup-based key initialization.
Creating the API Key Filter
Let’s create a global filter that validates API keys on every request:
@Slf4j
@Component
public class ApiKeyFilter implements GlobalFilter, Ordered {
public static final String X_API_KEY = "X-API-KEY";
private final ReactiveRedisTemplate<String, ApiKey> redisTemplate;
public ApiKeyFilter(ReactiveRedisTemplate<String, ApiKey> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String apiKeyValue = exchange.getRequest()
.getHeaders()
.getFirst(X_API_KEY);
if (apiKeyValue == null || apiKeyValue.isBlank()) {
return Mono.error(new ApiKeyNotFoundException("API key is missing"));
}
String path = exchange.getRequest().getPath().value();
String serviceName = extractServiceName(path);
return redisTemplate.opsForValue()
.get(RECORD_KEY + apiKeyValue)
.switchIfEmpty(Mono.error(new ApiKeyNotFoundException("API key not found")))
.flatMap(apiKey -> processApiKey(apiKey, serviceName, exchange, chain));
}
private Mono<Void> processApiKey(ApiKey apiKey, String serviceName,
ServerWebExchange exchange, GatewayFilterChain chain) {
if (isServiceAllowed(apiKey, serviceName)) {
log.debug("API key authorized for service {}", serviceName);
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-API-KEY-VALID", "true")
.header("X-API-KEY-ID", apiKey.getKey())
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} else {
log.warn("API key not allowed for service {}", serviceName);
return Mono.error(new AccessDeniedException("Access denied for this service"));
}
}
private boolean isServiceAllowed(ApiKey apiKey, String requestedServiceName) {
if (apiKey.getServices() == null) {
return false;
}
return apiKey.getServices().stream()
.anyMatch(service -> {
boolean nameMatches = service.getName().equalsIgnoreCase(requestedServiceName);
boolean notExpired = service.getExpiresAt() == null ||
service.getExpiresAt().isAfter(LocalDateTime.now());
return nameMatches && notExpired;
});
}
private String extractServiceName(String path) {
// Remove leading slash and get first segment
String cleanPath = path.startsWith("/") ? path.substring(1) : path;
String[] parts = cleanPath.split("/");
return parts.length > 0 ? parts[0] : "";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
Global filters run for all routes defined in the API Gateway. Here, we create a custom pre-filter that intercepts incoming requests and executes its logic before the request is routed by Spring Cloud Gateway to the downstream service.
This filter checks for the X-API-Key header and compares it to a predefined key.
Configuring Routes (Spring Cloud Gateway 4.x)
Configure your gateway routes in application.yml:
spring:
application:
name: spring-gateway-api-key
data:
redis:
url: redis://localhost:6379
cloud:
gateway.server.webflux:
routes:
# Customers service
- id: customers-service
uri: http://localhost:8081
predicates:
- Path=/customers/**
filters:
# Remove /customers prefix and add /api
- name: RewritePath
args:
regexp: /customers/(?<remaining>.*)
replacement: /customer-service/${remaining}
# Payments service
- id: payments-service
uri: http://localhost:8082
predicates:
- Path=/payments/**
filters:
- name: RewritePath
args:
regexp: /payments/(?<remaining>.*)
replacement: /payment-service/${remaining}
Microservices
Microservice A
This service simulates a customer microservice and exposes a mock controller instead of a real backend implementation.
@RestController
@RequestMapping(path = "/customer-service/clients")
public class ConsumerAController {
@GetMapping()
public List<String> customers(){
return List.of("CUST01", "CUST02");
}
}
Microservice B
This service simulates a payment microservice.
@RestController
@RequestMapping(path = "/payment-service/checkout")
public class ConsumerBController {
@GetMapping
public ResponseEntity<String> payment(){
return new ResponseEntity<>("payment api", HttpStatus.OK);
}
@PostMapping
public ResponseEntity<Payment> save(@RequestBody Payment payment){
return new ResponseEntity<>(payment, HttpStatus.CREATED);
}
}
Testing
At this point, we can run the gateway along with the microservices.
When the gateway starts up, the API key data is automatically initialized in Redis.

- When the client is not authorized to access the service using its API key.
- When the client is authorized to access the service.

- When the API key does not exist.
Conclusion
🏁 Well done !!.
Implementing API key authentication in Spring Cloud Gateway provides a straightforward yet effective security layer for your microservices. While not as feature-rich as OAuth2, API keys offer simplicity and ease of integration, making them ideal for specific use cases.
Remember to combine API key authentication with other security measures like HTTPS, rate limiting, and monitoring to create a robust security posture.
The complete source code is available on GitHub.
Thank you for reading!! See you in the next story.
References
- https://curity.io/resources/learn/the-api-security-maturity-model/
- https://nordicapis.com/introducing-the-api-security-maturity-model/
- https://docs.spring.io/spring-cloud-gateway/reference/index.html
- https://docs.spring.io/spring-data/redis/reference/
- https://owasp.org/www-project-api-security/
Last updated: December 18, 2025

