Skip to content

Gateway API

A Gateway API atua como ponto único de entrada para todos os microserviços do domínio store.
Ela é responsável por rotear, autenticar e auditar requisições externas, aplicando políticas de segurança e balanceando o tráfego entre os serviços internos.

Trusted layer e segurança

Toda requisição externa entra exclusivamente pelo Gateway.
O Gateway valida o JWT, injeta o id-account nos headers e redireciona a requisição para o microserviço correspondente (account, auth, order, product, exchange etc.).


Visão geral

  • Service (gateway-service): Implementado em Spring Cloud Gateway (Java).
    Centraliza o roteamento HTTP para os demais microserviços (account, auth, order, product, exchange).
    Aplica as regras de segurança via filtros customizados e validação de tokens JWT.

  • Segurança e Autorização
    Toda requisição passa por AuthorizationFilter, que valida o JWT e injeta o header id-account antes do encaminhamento ao destino.
    O RouterValidator define quais rotas exigem autenticação.
    O CorsFilter habilita o compartilhamento seguro entre domínios (CORS).

classDiagram
    class GatewayApplication {
        +main(String[] args)
    }

    class GatewayResource {
        +healthCheck(): String
    }

    class AuthorizationFilter {
        +filter(exchange, chain): Mono<Void>
        -isSecured(request): boolean
        -validateToken(token): Claims
        -injectHeaders(request, claims)
    }

    class RouterValidator {
        +isSecured(request): boolean
    }

    class CorsFilter {
        +filter(exchange, chain): Mono<Void>
    }

    GatewayApplication --> GatewayResource
    GatewayResource --> AuthorizationFilter
    GatewayApplication --> RouterValidator
    GatewayApplication --> CorsFilter

    note for AuthorizationFilter "Valida o JWT e injeta o header id-account"
    note for RouterValidator "Define rotas seguras / públicas"
    note for CorsFilter "Habilita o CORS para o domínio store"

Estrutura da requisição

flowchart LR
    subgraph api [Trusted Layer]
        direction TB
        gateway e2@==> account
        gateway e4@==> others
        account --> db@{ shape: cyl, label: "Database" }
        others --> db
    end
    internet e1@==>|request| gateway:::red
    e1@{ animate: true }
    e2@{ animate: true }
    e4@{ animate: true }
    classDef red fill:#fcc

Gateway-Service

📁 api
└── 📁 gateway-service/
    ├── 📁 k8s/
       └── 📄 k8s.yaml
    ├── 📁 src/
       └── 📁 main/
           ├── 📁 java/
              └── 📁 store/
                  └── 📁 gateway/
                      ├── 📄 GatewayApplication.java
                      ├── 📄 GatewayResource.java
                      └── 📁 security
                          ├── 📄 CorsFilter.java
                          ├── 📄 AuthorizationFilter.java
                          └── 📄 RouterValidator.java
           └── 📁 resources/
               └── 📄 application.yaml
    ├── 📄 pom.xml
    ├── 📄 Dockerfile
    └── 📄 Jenkinsfile
Source
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.5</version>
        <relativePath/>
    </parent>

    <groupId>store</groupId>
    <artifactId>gateway-service</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2025.0.0</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
1
2
3
4
FROM openjdk:23-slim
VOLUME /tmp
COPY target/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
pipeline {
    agent any
    environment {
        SERVICE = 'gateway'
        NAME = "jpqv/${env.SERVICE}"
        AWS_REGION  = "us-east-2"
        EKS_CLUSTER = "eks-store"
    }
    stages {
        stage('Build') { 
            steps {
                sh 'mvn -B -DskipTests clean package'
            }
        }      
        stage('Build & Push Image') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'dockerhub-credential',
                    usernameVariable: 'USERNAME',
                    passwordVariable: 'TOKEN')])
                {
                    sh "docker login -u $USERNAME -p $TOKEN"
                    sh "docker buildx create --use --platform=linux/arm64,linux/amd64 --node multi-platform-builder-${env.SERVICE} --name multi-platform-builder-${env.SERVICE}"
                    sh "docker buildx build --platform=linux/arm64,linux/amd64 --push --tag ${env.NAME}:latest --tag ${env.NAME}:${env.BUILD_ID} -f DockerFile ."
                    sh "docker buildx rm --force multi-platform-builder-${env.SERVICE}"
                }
            }
        }
        stage('Deploy to EKS') {
                steps {
                    withCredentials([[$class: 'AmazonWebServicesCredentialsBinding',
                    credentialsId: 'aws-credential',
                    accessKeyVariable: 'AWS_ACCESS_KEY_ID',
                    secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) 
                {
                    sh """
                        # garante diretório padrão do kubeconfig
                        mkdir -p ~/.kube

                        # configura contexto do cluster no caminho padrão (~/.kube/config)
                        aws eks update-kubeconfig --region ${AWS_REGION} --name ${EKS_CLUSTER}

                        kubectl config current-context

                        # aplica manifest inicial se ainda não existir
                        if ! kubectl get deploy ${SERVICE} >/dev/null 2>&1; then
                        kubectl apply -f ./k8s/k8s.yaml
                        fi

                        # atualiza a imagem do Deployment
                        kubectl set image deploy/${SERVICE} ${SERVICE}=${NAME}:${BUILD_ID} --record

                        # espera o rollout
                        kubectl rollout status deployment/${SERVICE} --timeout=180s
                    """
                }
            }
        }
    }
}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      containers:
        - name: gateway
          image: jpqv/gateway:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: "200Mi"
              cpu: "50m"
            limits:
              memory: "300Mi"
              cpu: "200m"

---

apiVersion: v1
kind: Service
metadata:
  name: gateway
  labels:
    app: gateway
spec:
  type: LoadBalancer
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8080

  selector:
    app: gateway
server:
  port: 8080

spring:
  application:
    name: gateway

  mvc:
    problemdetails:
      enabled: true

  cloud:
    gateway:
      server:
        webflux:
          routes:

            - id: insper
              uri: https://www.insper.edu.br
              predicates:
                - Path=/insper/**

            - id: account
              uri: http://account:8080
              predicates:
                - Path=/account/**

            - id: auth
              uri: http://auth:8080
              predicates:
                - Path=/auth/**

            - id: product
              uri: http://product:8080
              predicates:
                - Path=/product/**

            - id: order
              uri: http://order:8080
              predicates:
                - Path=/order/**

            - id: exchange
              uri: http://exchange:8080
              predicates:
                - Path=/exchange/**

          globalcors:
            corsConfigurations:
              '[/**]':
                allowedOrigins: "*"
                allowedHeaders: "*"
                allowedMethods: "*"

logging:
  level:
    root: info
    store: debug
package store.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

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

}
package store.gateway;

import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class GatewayResource {

    @GetMapping("/health-check")
    public ResponseEntity<Map<String, String>> healthCheck() {
        return ResponseEntity.ok()
            .body(Map.of(
                "osArch", System.getProperty("os.arch"),
                "osName", System.getProperty("os.name"),
                "osVersision", System.getProperty("os.version")
            ));
    }

    @GetMapping("/")
    public ResponseEntity<String> hello() {
        return ResponseEntity.ok()
            .body("API for Store");
    }

}
package store.gateway.security;

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 CorsFilter {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.setAllowCredentials(false);
        corsConfig.addAllowedMethod("*");
        corsConfig.addAllowedOrigin("*");
        corsConfig.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);

        return new CorsWebFilter(source);
    }

}
package store.gateway.security;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

@Component
public class AuthorizationFilter implements GlobalFilter {

    private Logger logger = LoggerFactory.getLogger(AuthorizationFilter.class);
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORIZATION_BEARER_HEADER = "Bearer";
    private static final String AUTH_SERVICE_TOKEN_SOLVE = "http://auth:8080/auth/solve";

    @Autowired
    private RouterValidator routerValidator;

    @Autowired
    private WebClient.Builder webClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        logger.debug("filter: entrou no filtro de autorizacao");
        ServerHttpRequest request = exchange.getRequest();

        if (!routerValidator.isSecured.test(request)) {
            logger.debug("filter: rota nao eh segura");
            return chain.filter(exchange);
        }
        logger.debug("filter: rota eh segura");

        if (!isAuthMissing(request)) {
            logger.debug("filter: tem [Authorization] no Header");
            String authorization = request.getHeaders().get(AUTHORIZATION_HEADER).get(0);
            logger.debug(String.format(
                "filter: [Authorization]=[%s]",
                authorization
            ));
            String[] parts = authorization.split(" ");
            if (parts.length != 2) {
                logger.debug("filter: bearer token is invalid");
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authorization header is not well formatted");
            }
            if (!AUTHORIZATION_BEARER_HEADER.equals(parts[0])) {
                logger.debug("filter: bearer token is invalid");
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authorization header is not well formatted");
            }
            logger.debug("filter: bearer token is formatted");

            final String jwt = parts[1];

            return requestAuthTokenSolve(exchange, chain, jwt);

        }
        logger.debug("filter: access is denied!");
        // if access is denied
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
    }

    private boolean isAuthMissing(ServerHttpRequest request) {
        return !request.getHeaders().containsKey(AUTHORIZATION_HEADER);
    }

    // este metodo eh responsavel por enviar o token ao Auth Microservice
    // a fim de interpretar o token, a chamada eh feita via Rest.
    private Mono<Void> requestAuthTokenSolve(ServerWebExchange exchange, GatewayFilterChain chain, String jwt) {
        logger.debug("solve: solving jwt: " + jwt);
        return webClient
            .defaultHeader(
                HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE
            )
            .build()
            .post()
            .uri(AUTH_SERVICE_TOKEN_SOLVE)
            .bodyValue(Map.of(
                "jwt", jwt)
            )
            .retrieve()
            .toEntity(Map.class)
            .flatMap(response -> {
                if (response != null && response.hasBody() && response.getBody() != null) {
                    final Map<String, String> map = response.getBody();
                    String idAccount = map.get("idAccount");
                    logger.debug("solve: id account: " + idAccount);
                    ServerWebExchange authorizated = updateRequest(exchange, idAccount);
                    return chain.filter(authorizated);
                } else {
                    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token");
                }
            });
    }

    private ServerWebExchange updateRequest(ServerWebExchange exchange, String idAccount) {
        logger.debug("original headers: " + exchange.getRequest().getHeaders().toString());
        ServerWebExchange modified = exchange.mutate()
            .request(
                exchange.getRequest()
                    .mutate()
                    .header("id-account", idAccount)
                    .build()
            ).build();
        logger.debug("updated headers: " + modified.getRequest().getHeaders().toString());
        return modified;
    }    

}
package store.gateway.security;

import java.util.List;
import java.util.function.Predicate;

import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

@Component
public class RouterValidator {

        private List<String> openApiEndpoints = List.of(
                "POST /auth/register",
                "POST /auth/login"
        );

        public Predicate<ServerHttpRequest> isSecured =
                request -> openApiEndpoints
                        .stream()
                        .noneMatch(uri -> {
                                String[] parts = uri.replaceAll("[^a-zA-Z0-9// *]", "").split(" ");
                                final String method = parts[0];
                                final String path = parts[1];
                                final boolean deep = path.endsWith("/**");
                                return ("ANY".equalsIgnoreCase(method) || request.getMethod().toString().equalsIgnoreCase(method))
                                        && (request.getURI().getPath().equals(path) || (deep && request.getURI().getPath().startsWith(path.replace("/**", ""))));
                        });

}
mvn clean package spring-boot:run