Published on

9 - Simple Microservices part one

Authors
  • avatar
    Name
    Samreach YAN
    Twitter

Table of Contents

  1. Project Structure
  2. Docker Compose Setup
  3. Microservice Architecture Principles
  4. Service Discovery with Eureka
  5. Configuration Management
  6. Circuit Breakers with Resilience4j
  7. API Gateway Implementation
  8. Swagger/OpenAPI Documentation
  9. Spring Security with OAuth2 and Keycloak
  10. Rate Limiting with Redis
  11. Failover and Retry Policies
  12. Keycloak Integration
  13. Final Deployment

Prerequisites

  • Java 17+
  • Spring Boot 3.x
  • Basic Spring Security knowledge
  • Docker (for PostgreSQL setup)

1. Project Structure

microservices-advanced/
├── api-gateway/
│   ├── src/main/
│   │   ├── java/com/example/apigateway/
│   │   │   ├── config/
│   │   │   ├── controller/
│   │   │   └── ApiGatewayApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── bootstrap.yml
│   └── pom.xml
├── config-server/
│   ├── src/main/
│   │   ├── java/com/example/configserver/
│   │   │   └── ConfigServerApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── bootstrap.yml
│   └── pom.xml
├── discovery-server/
│   ├── src/main/
│   │   ├── java/com/example/discoveryserver/
│   │   │   ├── config/
│   │   │   └── DiscoveryServerApplication.java
│   │   └── resources/
│   │       └── application.yml
│   └── pom.xml
├── user-service/
│   ├── src/main/
│   │   ├── java/com/example/userservice/
│   │   │   ├── config/
│   │   │   ├── controller/
│   │   │   ├── dto/
│   │   │   ├── model/
│   │   │   ├── repository/
│   │   │   ├── service/
│   │   │   └── UserServiceApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── bootstrap.yml
│   └── pom.xml
├── order-service/
│   ├── src/main/
│   │   ├── java/com/example/orderservice/
│   │   │   ├── config/
│   │   │   ├── controller/
│   │   │   ├── dto/
│   │   │   ├── model/
│   │   │   ├── repository/
│   │   │   ├── service/
│   │   │   └── OrderServiceApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── bootstrap.yml
│   └── pom.xml
├── docker-compose.yml
└── README.md

2. Docker Compose Setup

services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres
    environment:
      POSTGRES_PASSWORD: admin
      POSTGRES_USER: admin
      POSTGRES_DB: microservices
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U admin']
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - microservices-network

  redis:
    image: redis:7-alpine
    container_name: redis
    ports:
      - '6379:6379'
    volumes:
      - redis_data:/data
    networks:
      - microservices-network

  keycloak:
    image: quay.io/keycloak/keycloak:21.1.1
    container_name: keycloak
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/microservices
      KC_DB_USERNAME: admin
      KC_DB_PASSWORD: admin
      KC_HOSTNAME: keycloak
    ports:
      - '8181:8080'
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - microservices-network
    command: ['start-dev']

  discovery-server:
    build: ./discovery-server
    container_name: discovery-server
    ports:
      - '8761:8761'
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - postgres
    networks:
      - microservices-network

  config-server:
    build: ./config-server
    container_name: config-server
    ports:
      - '8888:8888'
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - discovery-server
    networks:
      - microservices-network

  api-gateway:
    build: ./api-gateway
    container_name: api-gateway
    ports:
      - '8080:8080'
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - discovery-server
      - config-server
      - redis
    networks:
      - microservices-network

  user-service:
    build: ./user-service
    container_name: user-service
    ports:
      - '9001:9001'
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - discovery-server
      - config-server
      - postgres
      - keycloak
    networks:
      - microservices-network

  order-service:
    build: ./order-service
    container_name: order-service
    ports:
      - '9002:9002'
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - discovery-server
      - config-server
      - postgres
      - keycloak
    networks:
      - microservices-network

volumes:
  postgres_data:
  redis_data:

networks:
  microservices-network:
    driver: bridge

3. Microservice Architecture Principles

Key principles implemented:

  1. Single Responsibility Principle: Each service handles one business capability
  2. Decentralized Data Management: Each service has its own database schema
  3. Infrastructure Automation: Dockerized with health checks
  4. Design for Failure: Circuit breakers, retries, fallbacks
  5. Evolutionary Design: Independent deployability

4. Service Discovery with Eureka

discovery-server/pom.xml

<?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.1.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>discovery-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>discovery-server</name>
    <description>Service Discovery Server</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </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>
            </plugin>
        </plugins>
    </build>
</project>

discovery-server/src/main/java/com/example/discoveryserver/config/SecurityConfig.java

package com.example.discoveryserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("admin")
            .roles("ADMIN")
            .build();
        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }
}

discovery-server/src/main/resources/application.yml

server:
  port: 8761

eureka:
  instance:
    hostname: discovery-server
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 5000

spring:
  application:
    name: discovery-server
  security:
    user:
      name: admin
      password: admin

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

5. Configuration Management

config-server/pom.xml

<?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.1.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>config-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>config-server</name>
    <description>Configuration Server</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </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>
            </plugin>
        </plugins>
    </build>
</project>

config-server/src/main/resources/application.yml

server:
  port: 8888

spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-repo/microservices-config
          clone-on-start: true
          search-paths: '{application}'
  security:
    user:
      name: configUser
      password: configPassword

eureka:
  client:
    serviceUrl:
      defaultZone: http://admin:admin@discovery-server:8761/eureka/
  instance:
    preferIpAddress: true

management:
  endpoints:
    web:
      exposure:
        include: '*'

6. Circuit Breakers with Resilience4j

order-service/pom.xml (additional dependencies)

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

order-service/src/main/resources/application.yml

resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 5
        permittedNumberOfCallsInHalfOpenState: 3
        automaticTransitionFromOpenToHalfOpenEnabled: true
        waitDurationInOpenState: 5s
        failureRateThreshold: 50
        eventConsumerBufferSize: 10
    instances:
      productService:
        baseConfig: default

order-service/src/main/java/com/example/orderservice/service/OrderService.java

package com.example.orderservice.service;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderService {

    private static final String PRODUCT_SERVICE = "productService";
    private final RestTemplate restTemplate;

    public OrderService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @CircuitBreaker(name = PRODUCT_SERVICE, fallbackMethod = "getProductDetailsFallback")
    public String getProductDetails(String productId) {
        return restTemplate.getForObject(
            "http://product-service/api/products/" + productId,
            String.class
        );
    }

    public String getProductDetailsFallback(String productId, Exception e) {
        return "Product service is currently unavailable. Please try again later.";
    }
}

7. API Gateway Implementation

api-gateway/pom.xml

<?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.1.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>api-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>api-gateway</name>
    <description>API Gateway</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
        <resilience4j.version>2.1.0</resilience4j.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-spring-boot3</artifactId>
            <version>${resilience4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </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>
            </plugin>
        </plugins>
    </build>
</project>

api-gateway/src/main/resources/application.yml

server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - name: CircuitBreaker
              args:
                name: userService
                fallbackUri: forward:/fallback/user-service
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                redis-rate-limiter.requestedTokens: 1
            - StripPrefix=2
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: CircuitBreaker
              args:
                name: orderService
                fallbackUri: forward:/fallback/order-service
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY, INTERNAL_SERVER_ERROR
                methods: GET,POST
                backoff:
                  firstBackoff: 10ms
                  maxBackoff: 1000ms
                  factor: 2
                  basedOnPreviousValue: false
            - StripPrefix=2
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://keycloak:8080/realms/microservices-realm
          jwk-set-uri: http://keycloak:8080/realms/microservices-realm/protocol/openid-connect/certs
  redis:
    host: redis
    port: 6379

eureka:
  client:
    serviceUrl:
      defaultZone: http://admin:admin@discovery-server:8761/eureka/
  instance:
    preferIpAddress: true

resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 5
        permittedNumberOfCallsInHalfOpenState: 3
        automaticTransitionFromOpenToHalfOpenEnabled: true
        waitDurationInOpenState: 5s
        failureRateThreshold: 50
        eventConsumerBufferSize: 10
    instances:
      userService:
        baseConfig: default
      orderService:
        baseConfig: default

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

8. Swagger/OpenAPI Documentation

api-gateway/src/main/java/com/example/apigateway/config/OpenApiConfig.java

package com.example.apigateway.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

    private static final String SECURITY_SCHEME_NAME = "bearerAuth";

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
            .components(
                new Components()
                    .addSecuritySchemes(SECURITY_SCHEME_NAME,
                        new SecurityScheme()
                            .name(SECURITY_SCHEME_NAME)
                            .type(SecurityScheme.Type.HTTP)
                            .scheme("bearer")
                            .bearerFormat("JWT")
                    )
            )
            .info(new Info()
                .title("Microservices API Gateway")
                .version("1.0")
                .description("Documentation for all microservices via API Gateway")
                .contact(new Contact()
                    .name("API Support")
                    .email("support@example.com"))
                .license(new License()
                    .name("Apache 2.0")
                    .url("https://www.apache.org/licenses/LICENSE-2.0.html")));
    }
}

9. Spring Security with OAuth2 and Keycloak

user-service/pom.xml

<?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.1.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>user-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user-service</name>
    <description>User Service</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2022.0.3</spring-cloud.version>
        <keycloak.version>21.1.1</keycloak.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>${keycloak.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </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>
            </plugin>
        </plugins>
    </build>
</project>

user-service/src/main/java/com/example/userservice/config/SecurityConfig.java

package com.example.userservice.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@KeycloakConfiguration
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = new KeycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/users/register").permitAll()
                .requestMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt());
        return http.build();
    }
}

10. Rate Limiting with Redis

The rate limiting is already configured in the API Gateway's application.yml:

- name: RequestRateLimiter
  args:
    redis-rate-limiter.replenishRate: 10
    redis-rate-limiter.burstCapacity: 20
    redis-rate-limiter.requestedTokens: 1

11. Failover and Retry Policies

api-gateway/src/main/java/com/example/apigateway/controller/FallbackController.java

package com.example.apigateway.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/user-service")
    public Mono<ResponseEntity<Map<String, String>>> userServiceFallback() {
        Map<String, String> response = new HashMap<>();
        response.put("message", "User Service is taking too long to respond or is down. Please try again later");
        response.put("status", HttpStatus.SERVICE_UNAVAILABLE.toString());
        return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response));
    }

    @GetMapping("/order-service")
    public Mono<ResponseEntity<Map<String, String>>> orderServiceFallback() {
        Map<String, String> response = new HashMap<>();
        response.put("message", "Order Service is taking too long to respond or is down. Please try again later");
        response.put("status", HttpStatus.SERVICE_UNAVAILABLE.toString());
        return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response));
    }
}

12. Keycloak Integration

Keycloak Initial Setup

  1. Access Keycloak admin console at http://localhost:8181
  2. Login with admin/admin
  3. Create a new realm called "microservices-realm"
  4. Create a client for each service:

API Gateway Client

  • Client ID: api-gateway
  • Client Protocol: openid-connect
  • Access Type: confidential
  • Valid Redirect URIs: *
  • Web Origins: *
  • Authorization Enabled: On

User Service Client

  • Client ID: user-service
  • Client Protocol: openid-connect
  • Access Type: confidential
  • Service Accounts Enabled: On
  • Authorization Enabled: On

Order Service Client

  • Client ID: order-service
  • Client Protocol: openid-connect
  • Access Type: confidential
  • Service Accounts Enabled: On
  • Authorization Enabled: On

user-service/src/main/resources/application.yml

server:
  port: 9001

spring:
  application:
    name: user-service
  datasource:
    url: jdbc:postgresql://postgres:5432/microservices
    username: admin
    password: admin
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect

keycloak:
  auth-server-url: http://keycloak:8080
  realm: microservices-realm
  resource: user-service
  credentials:
    secret: ${KEYCLOAK_CLIENT_SECRET}
  bearer-only: true
  ssl-required: external
  use-resource-role-mappings: true

eureka:
  client:
    serviceUrl:
      defaultZone: http://admin:admin@discovery-server:8761/eureka/
  instance:
    preferIpAddress: true

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

13. Final Deployment

Build and Run Commands

# Build all services
docker-compose build

# Start all services
docker-compose up -d

# Scale services
docker-compose up -d --scale user-service=2 --scale order-service=2

# View logs
docker-compose logs -f

# Check service health
docker-compose ps

# Stop services
docker-compose down -v

Testing the System

  1. Access Eureka Dashboard: http://localhost:8761 (admin/admin)
  2. Access API Gateway Swagger: http://localhost:8080/swagger-ui.html
  3. Get Keycloak token:
curl -X POST \
  http://localhost:8181/realms/microservices-realm/protocol/openid-connect/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'client_id=api-gateway&client_secret=<YOUR_CLIENT_SECRET>&grant_type=password&username=<USERNAME>&password=<PASSWORD>'
  1. Use the access token to call protected endpoints through the API Gateway.