Connecting Your Java Spring Boot Microservices Seamlessly with Spring Cloud Gateway: A Step-by-Step Guide

Nishipal RanaNishipal Rana
7 min read

In one of the projects, I was tasked with replacing the Netflix Zuul Gateway with Spring Cloud Gateway. While migrating the gateway it was difficult to find resources for Spring Cloud Gateway implementation. Hence this blog is an attempt to pour all my experience and learnings which I acquired while implementing Spring Cloud Gateway for the project's multiple microservices. Integrating Spring Cloud Gateway with your Java microservices is a powerful way to manage and secure your APIs. Spring Cloud Gateway provides several benefits including routing, rate limiting, load balancing, and security.

The project implements Spring Cloud Gateway with Netflix Eureka Registry for multiple microservices.

The Project contains 4 Gradle projects:-

  • Eureka Service Registry

  • Spring Cloud Gateway Service

  • Eureka Client Service

  • Auth Service

1. First, create a new Spring Boot application with the necessary dependencies:

package com.spring.boot.cloud.gateway;



import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;


@EnableEurekaClient
@SpringBootApplication
public class SpringCloudGatewayServiceApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudGatewayServiceApplication.class, args);
    }
}

2. Next, configure Spring Cloud Gateway by creating a RouteLocator bean that defines the routes for your microservices:

package com.spring.boot.cloud.gateway.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.spring.boot.cloud.gateway.filter.JwtAuthenticationFilter;

@Configuration
public class GatewayConfig extends GatewayAutoConfiguration{

    @Autowired
    private JwtAuthenticationFilter jwtFilter;

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/client-service/getMessage/**").filters(f -> f.rewritePath("/client-service/(?<path>.*)","/${path}")).uri("lb://spring-cloud-eureka-client"))
                .route(r -> r.path("/client-service/postMessage").filters(f -> f.rewritePath("/client-service/(?<path>.*)","/${path}")).uri("lb://spring-cloud-eureka-client"))
                .route(r -> r.path("/auth-service/auth/login").filters(f -> f.rewritePath("/auth-service/(?<path>.*)","/${path}")).uri("lb://spring-cloud-eureka-auth"))
                .route(r -> r.path("/auth-service/auth/register").filters(f -> f.rewritePath("/auth-service/(?<path>.*)","/${path}")).uri("lb://spring-cloud-eureka-auth"))
                .build();
    }

}

In this example, the RouteLocator bean defines four routes - two for client-service, and another two for auth-service. Each route specifies a URL pattern, filters to modify the request, and a URI for the microservice.

3. Configure filters, for example, to add authentication, here I have added a jwtAuth filter to all the network requests that are intercepted by the gateway. The filter can check the request for a valid token or user credentials to ensure proper authentication.

package com.spring.boot.cloud.gateway.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.spring.boot.cloud.gateway.filter.JwtAuthenticationFilter;

@Configuration
public class GatewayConfig extends GatewayAutoConfiguration{

    @Autowired
    private JwtAuthenticationFilter jwtFilter;

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/client-service/getMessage/**").filters(f -> f.rewritePath("/client-service/(?<path>.*)","/${path}").filter(jwtFilter)).uri("lb://spring-cloud-eureka-client"))
                .route(r -> r.path("/client-service/postMessage").filters(f -> f.rewritePath("/client-service/(?<path>.*)","/${path}").filter(jwtFilter)).uri("lb://spring-cloud-eureka-client"))
                .route(r -> r.path("/auth-service/auth/login").filters(f -> f.rewritePath("/auth-service/(?<path>.*)","/${path}").filter(jwtFilter)).uri("lb://spring-cloud-eureka-auth"))
                .route(r -> r.path("/auth-service/auth/register").filters(f -> f.rewritePath("/auth-service/(?<path>.*)","/${path}").filter(jwtFilter)).uri("lb://spring-cloud-eureka-auth"))
                .build();
    }

}

4. We add another configuration related to CORS (Cross-Origin Resource Sharing) for Spring Cloud Gateway. It prevents malicious scripts from accessing or manipulating sensitive data from different domains. The UrlBasedCorsConfigurationSource bean registers the configuration for all endpoints. The CorsWebFilter bean applies the configuration to incoming requests.

package com.spring.boot.cloud.gateway.config;

import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsConfigFilter extends CorsConfiguration {

    @Bean
    public CorsWebFilter corsFilter() {
        return new CorsWebFilter(corsConfigurationSource());
    }

    /* Note: The special value "*" allows all domains.
     * In case of you want to set setallowCredentials() to true then "*" cannot be set as value in allowed origins.
     */

    @Bean
    UrlBasedCorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.applyPermitDefaultValues(); //Set the default values

        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","OPTIONS","DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("*"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

To enable CORS in Spring Cloud Gateway, the CorsConfigFilter class is created with the @Configuration annotation. This class extends the CorsConfiguration class, which contains the default configuration for CORS requests.

The corsFilter() method is annotated with the @Bean annotation, which registers a bean in the application context. This method returns an instance of CorsWebFilter, which is responsible for filtering the CORS requests.

The corsConfigurationSource() method creates a new instance of CorsConfiguration and sets the allowed origins, methods, and headers. The setAllowedOrigins() method sets the origins that are allowed to access the resources. In this example, the "*" value allows all domains. However, if you want to set the `setAllowCredentials()` method to true, then you cannot set " *" as the value in allowed origins.

The setAllowedMethods() method sets the HTTP methods that are allowed to access the resources. The setAllowedHeaders() method sets the headers that are allowed in the CORS request.

Finally, the UrlBasedCorsConfigurationSource class creates a new configuration source and registers the CorsConfiguration instance to the "/**" path. This means that the CORS configuration will apply to all endpoints in the application.

By adding this configuration file to your Spring Cloud Gateway project, you can ensure that all incoming requests comply with the CORS security standard, improving the overall security and reliability of your microservices architecture.

5. The filter is another important aspect of this implementation and you can customize it based on your business logic. The filter can act as both a Pre-filter and a post-filter.

JwtAuthenticationFilter is a filter component that is responsible for authenticating incoming requests to the gateway service by verifying the presence and validity of a JWT token in the request header.

The filter first checks if the request path contains either "login" or "register". If it does, it allows the request to pass through without authentication. Otherwise, it checks if the request header contains an "Authorization" token. If the token is missing, it returns an unauthorized response to the client.

If the token is present, the filter uses the JwtUtil component to validate the token. If the token is invalid or malformed, it returns a bad request response to the client with an error message.

If the token is valid, the filter extracts the Claims object from the token and add the "id" header to the request with the value of the subject claim. This allows downstream services to access the authenticated user's ID.

After the request is authenticated, the filter calls the chain.filter(exchange) method to pass the request to the next filter in the chain or the destination service if no more filters are configured. Once a response is received from the downstream service, the filter executes its post-filter logic and returns the response to the client.

package com.spring.boot.cloud.gateway.filter;

import java.nio.charset.StandardCharsets;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
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.web.server.ServerWebExchange;

import com.google.gson.JsonObject;
import com.spring.boot.cloud.gateway.exception.JwtTokenMalformedException;
import com.spring.boot.cloud.gateway.exception.JwtTokenMissingException;
import com.spring.boot.cloud.gateway.util.JwtUtil;

import io.jsonwebtoken.Claims;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class JwtAuthenticationFilter implements GatewayFilter {

    @Autowired
    private JwtUtil jwtUtil;

    //Creating the Logger object
    Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = (ServerHttpRequest) exchange.getRequest();
        logger.info("Gateway Pre Filter executed");
        if  (!(request.getURI().getPath().contains("login") || request.getURI().getPath().contains("register"))){
            if (!request.getHeaders().containsKey("Authorization")) {
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return sendErrorResponse(exchange, "Authorization Not Found");
            }

            final String token = request.getHeaders().getOrEmpty("Authorization").get(0);

            try {
                jwtUtil.validateToken(token);
            } catch (JwtTokenMalformedException | JwtTokenMissingException e) {
                logger.info("Exception "+e.getMessage());
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.BAD_REQUEST);
                return sendErrorResponse(exchange, e.getMessage());
            }

            Claims claims = jwtUtil.getClaims(token);
            exchange.getRequest().mutate().header("id", String.valueOf(claims.getSubject())).build();

        }

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            logger.info("Post Filter -> Executes after the response is received from the called service");
        }));
    }

}

6. We also create a custom method called sendErrorResponse which is responsible for creating and sending the error response. It creates a JSON object with an error message, status code, path, and request ID. The response is sent back to the client as a JSON string.

The getJsonResponseObj method is a utility method that creates a JSON object with the error message, status code, path, and request ID. This method is called by the sendErrorResponse method to create the error response.

publ Mono<Void> sendErrorResponse(ServerWebExchange exchange, String errMessage){

        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json");

        byte[] bytes = getJsonResponseObj(errMessage, HttpStatus.UNAUTHORIZED.value(), request.getPath().toString(), request.getId()).toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        return response.writeWith(Flux.just(buffer));
        }

    public JsonObject getJsonResponseObj(String errorMessage,int errorCode, String path, String requestId) {
        JsonObject errResponse = new JsonObject();
        errResponse.addProperty("message", errorMessage);
        errResponse.addProperty("status", errorCode);
        errResponse.addProperty("path", path);
        errResponse.addProperty("requestId", requestId);
        return errResponse;
    }

7. Lastly we need to add application.yml to configure the Spring application name, the port for the service, the Eureka client configuration for service discovery, and most importantly, the global CORS (Cross-Origin Resource Sharing) configuration for the gateway.

spring:
  application:
    name: spring-cloud-gateway-service
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods:
            - GET
            - POST
            - PUT
            - DELETE
            - OPTIONS
server:
  port: 8800
eureka:
  client:
    serviceUrl:
      defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
  instance:
    hostname: localhost
    preferIpAddress: true

That's it! With Spring Cloud Gateway, you can easily integrate and manage your Java microservices for improved performance and security. You can find the source code for the whole project on my GitHub

🖱️Follow Nishipal Rana for more insightful articles!

0
Subscribe to my newsletter

Read articles from Nishipal Rana directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nishipal Rana
Nishipal Rana

Hi, I'm Nishipal Y. Rana, a Full stack Developer with 2.5 years of experience creating seamless user experiences across various platforms. I'm passionate about problem-solving and dedicated to delivering efficient, scalable solutions. Check out my blog and website for more information about my skills and approach. Let's connect and explore collaboration opportunities!