developers

Java Microservices with Spring Boot and Spring Cloud

This tutorial shows you how to build a microservices architecture with Spring Boot and Spring Cloud.

Oct 2, 202318 min read

Adopting a microservices architecture provides unique opportunities to add failover and resiliency to your systems so your components can gracefully handle load spikes and errors. Microservices make change less expensive, too. They can also be a good idea when you have a large team working on a single product. You can break up your project into components that can function independently. Once components can operate independently, they can be built, tested, and deployed separately. This gives an organization and its teams the agility to develop and deploy quickly.

Java is an excellent language with a vast open source ecosystem for developing a microservice architecture. In fact, some of the biggest names in our industry use Java and contribute to its ecosystem. Have you ever heard of Netflix, Amazon, or Google? What about eBay, Twitter, and LinkedIn? Yes, web-scale companies handling incredible traffic are doing it with Java.

Implementing a microservices architecture in Java isn't for everyone. For that matter, implementing microservices, in general, isn't often needed. Most companies do it to scale their people, not their systems. Even Martin Fowler's original blog post on Microservices recommends against it:

One reasonable argument we've heard is that you shouldn't start with a microservices architecture. Instead begin with a monolith, keep it modular, and split it into microservices once the monolith becomes a problem.

The Java ecosystem has some well-established patterns for developing microservice architectures. If you're familiar with Spring, you'll feel right at home developing with Spring Boot and Spring Cloud. Since that's one of the quickest ways to get started, I figured I'd walk you through a quick example.

This example contains a microservice with a REST API that returns a list of cool cars. It uses Netflix Eureka for service discovery, WebClient for remote communication, and Spring Cloud Gateway to route requests to the microservice. It integrates Spring Security and OAuth 2.0, so only authenticated users can access the API gateway and microservice. It also uses Resilience4j to add fault tolerance to the gateway.

Here is a diagram showing the overall infrastructure:

The microservice infrastructure

Create Java Microservices with Spring Boot and Spring Cloud

I like to show developers how to build everything from scratch. Today, I'm going to take a different approach. First, I'll show you how to get the completed example working. Then, I'll explain how I created everything and the trials and tribulations I encountered along the way.

You can start by cloning the @oktadev/auth0-java-microservices-examples repository.

git clone https://github.com/oktadev/auth0-java-microservices-examples

There are two directories in this repository that pertain to this tutorial:

  • spring-boot-gateway-webflux: a Spring Boot microservice architecture with Spring Cloud Gateway and Spring WebFlux.
  • spring-boot-gateway-mvc: a Spring Boot microservice architecture with Spring Cloud Gateway and Spring MVC.

Each directory contains three projects:

  • discovery-service: a Netflix Eureka server used for service discovery.
  • car-service: a simple Car Service that uses Spring Data REST to serve up a REST API of cars.
  • api-gateway: an API gateway with a
    /cool-cars
    endpoint that talks to the car service and filters out cars that aren't cool (in my opinion, of course).

The configuration for the WebFlux and MVC implementations is the same, so choose one and follow along.

You can also watch a demo of the WebFlux example in the screencast below.

Run a Secure Spring Boot Microservice Architecture

To run the example, you must install the Auth0 CLI and create an Auth0 account. If you don't have an Auth0 account, sign up for free. I recommend using SDKMAN! to install Java 17+ and HTTPie for making HTTP requests.

First, start the discovery service:

cd discovery-service
./gradlew bootRun

Before you can start the API gateway project, you'll need to configure the API gateway to use your Auth0 account.

Open a terminal and run

auth0 login
to configure the Auth0 CLI to get an API key for your tenant. Then, run
auth0 apps create
to register an OpenID Connect (OIDC) app with the appropriate URLs:

auth0 apps create \
  --name "Kick-Ass Cars" \
  --description "Microservices for Cool Cars" \
  --type regular \
  --callbacks http://localhost:8080/login/oauth2/code/okta \
  --logout-urls http://localhost:8080 \
  --reveal-secrets

Copy

api-gateway/.env.example
to
.env
and edit it to contain the values from the command above.

OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
OKTA_OAUTH2_CLIENT_ID=
OKTA_OAUTH2_CLIENT_SECRET=
OKTA_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/

At startup, these properties will be read using spring-dotenv.

Run

./gradlew bootRun
to start the API gateway, or use your IDE to run it.

Copy

car-service/.env.example
to
.env
and update its values.

OKTA_OAUTH2_ISSUER=https://<your-auth0-domain>/
OKTA_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/

Start it with

./gradlew bootRun
and open
http://localhost:8080
in your favorite browser. You'll be redirected to Auth0 to log in:

Auth0 Universal Login page

After authenticating, you'll see your name in lights! ✨

Your name in lights

You can navigate to the following URLs in your browser for different results:

  • http://localhost:8080/print-token
    : prints access token to the terminal
  • http://localhost:8080/cool-cars
    : returns a list of cool cars
  • http://localhost:8080/home
    : proxies request to the car service and prints JWT claims in this application's terminal

You can see the access token's contents by copying/pasting it into jwt.io. You can also access the car service directly using it.

TOKEN=<access-token>
http :8090/cars Authorization:"Bearer $TOKEN"

Call the car service with an access token

Pretty cool, eh? 😎

My Developer Story with Spring Boot and Spring Cloud

A few years ago, I created an example similar to this one with Spring Boot 2.2. It used Feign for remote connectivity, Zuul for routing, Hystrix for failover, and Spring Security for OAuth. The September 2023 version of Spring Cloud has Spring Cloud OpenFeign for remote connectivity, Spring Cloud Gateway for routing, and Resilience4j for fault tolerance.

Okta also now has an Okta Spring Boot starter. I didn't use it in my first experiment, but I'm a big fan of it after the last few years! It dramatically simplifies configuration and makes securing your apps with OAuth 2.0 and OIDC easy. It's a thin wrapper around Spring Security's resource server, OAuth client, and OIDC features. Not only that, but it works with Okta Workforce Identity, Okta Customer Identity (aka Auth0), and even Keycloak.

I created all of these applications using start.spring.io's REST API and HTTPie.

https start.spring.io/starter.tgz bootVersion==3.2.0 \
  artifactId==discovery-service name==eureka-service \
  dependencies==cloud-eureka-server baseDir==discovery-service | tar -xzvf -

https start.spring.io/starter.tgz bootVersion==3.2.0 \
  artifactId==car-service name==car-service baseDir==car-service \
  dependencies==actuator,cloud-eureka,data-jpa,data-rest,postgresql,web,validation,devtools,docker-compose,okta | tar -xzvf -

https start.spring.io/starter.tgz bootVersion==3.2.0 \
  artifactId==api-gateway name==api-gateway baseDir==api-gateway \
  dependencies==cloud-eureka,cloud-feign,data-rest,web,okta | tar -xzvf -

You might notice the

api-gateway
project doesn't have
cloud-gateway
as a dependency. That's because I started without it and didn't add it until I needed to proxy requests by path.

In the code listings below, all

package
and
import
statements have been removed for brevity. You can find the complete source code in the auth0-java-microservices-examples repository.

Service Discovery with Netflix Eureka

The

discovery-service
is configured the same as you would most Eureka servers. It has an
@EnableEurekaServer
annotation on its main class and properties that set its port and turn off discovery.

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

The

car-service
and
api-gateway
projects are configured similarly. Both have a unique name defined, and
car-service
is configured to run on port
8090
so it doesn't conflict with
8080
:

# car-service/src/main/resources/application.properties
server.port=8090
spring.application.name=car-service
# api-gateway/src/main/resources/application.properties
spring.application.name=api-gateway

@EnableDiscoveryClient
annotates the main class in both car service and API gateway.

Build a Java Microservice with Spring Data REST

The

car-service
provides a REST API that lets you CRUD (Create, Read, Update, and Delete) cars. It creates a default set of cars when the application loads using an
ApplicationRunner
bean:

// car-service/src/main/java/com/example/carservice/CarServiceApplication.java
@EnableDiscoveryClient
@SpringBootApplication
public class CarServiceApplication {

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

    @Bean
    ApplicationRunner init(CarRepository repository) {
        repository.deleteAll();
        return args -> {
            Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti",
                "AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> {
                repository.save(new Car(name));
            });
            repository.findAll().forEach(System.out::println);
        };
    }
}

The

CarRepository
interface makes persisting and fetching cars from the database easy:

// car-service/src/main/java/com/example/carservice/data/CarRepository.java
@RepositoryRestResource
public interface CarRepository extends JpaRepository<Car, Long> {
}

The

Car
class is a simple JPA entity with an
id
and
name
property. Spring Boot will see PostgreSQL on its classpath and autoconfigure connectivity. A
compose.yaml
file exists in the root directory to start a PostgreSQL instance using Docker Compose:

services:
  postgres:
    image: 'postgres:latest'
    environment:
      - 'POSTGRES_DB=mydatabase'
      - 'POSTGRES_PASSWORD=secret'
      - 'POSTGRES_USER=myuser'
    ports:
      - '5432'

Spring Boot added Docker Compose support in version 3.1. This means that if you add the following dependency to your

build.gradle
, it'll look for a
compose.yaml
(or
docker-compose.yaml
) file in the root directory and start it when you run
./gradlew bootRun
:

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'

Finally, the

application.properties
has a setting to create the database automatically:

spring.jpa.hibernate.ddl-auto=update

Connect to Java Microservices with Spring Cloud OpenFeign

Next, I configured OpenFeign in the

api-gateway
project to connect to the car service and its
/cars
endpoint. Then, I mapped a
Car
record to the JSON that's returned. I exposed it as a
/cool-cars
endpoint:

// api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {

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

record Car(String name) {
}

@FeignClient("car-service")
interface CarClient {

    @GetMapping("/cars")
    CollectionModel<Car> readCars();
}

@RestController
class CoolCarController {

    private final CarClient carClient;

    public CoolCarController(CarClient carClient) {
        this.carClient = carClient;
    }

    @GetMapping("/cool-cars")
    public Collection<Car> coolCars() {
        return carClient.readCars()
                .getContent()
                .stream()
                .filter(this::isCool)
                .collect(Collectors.toList());
    }

    private boolean isCool(Car car) {
        return !car.name().equals("AMC Gremlin") &&
                !car.name().equals("Triumph Stag") &&
                !car.name().equals("Ford Pinto") &&
                !car.name().equals("Yugo GV");
    }
}

This worked great, but I still wanted to proxy

/home
to the downstream car service.

Add Routing with Spring Cloud Gateway

When I first wrote this tutorial with Spring Boot 3.1 and Spring Cloud 2022.0.4, Spring Cloud Gateway only had a WebFlux API. Since Spring Cloud 2023.0.0, it has a Spring MVC API too! This means you can use it with Spring MVC or Spring WebFlux.

Proxy Requests by Path with Spring MVC

I added

spring-cloud-starter-gateway-mvc
as a dependency to the
api-gateway
project and added the following to a new
api-gateway/src/main/resources/application.yml
file:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      mvc:
        routes:
          - id: car-service
            uri: lb://car-service
            predicates:
              - Path=/home/**

With this configuration, I could access the car service directly at

http://localhost:8090/cars
and through the gateway at
http://localhost:8080/cool-cars
.

To add failover with Spring Cloud Circuit Breaker, I added it as a dependency:

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

Then, I enabled it in

application.properties
:

spring.cloud.openfeign.circuitbreaker.enabled=true

I updated the

CarClient
interface in
ApiGatewayApplication
to have a fallback that returns an empty list of cars if the car service is unavailable.

@FeignClient(name = "car-service", fallback = Fallback.class)
interface CarClient {

    @GetMapping("/cars")
    CollectionModel<Car> readCars();

}

@Component
class Fallback implements CarClient {

    @Override
    public CollectionModel<Car> readCars() {
        return CollectionModel.empty();
    }
}

Proxy Requests by Path with WebFlux

Getting everything to work with Spring MVC and Spring Cloud Gateway didn't take long. Using Spring WebFlux required a bit more work.

I immediately discovered that adding

spring-cloud-starter-gateway
as a dependency caused issues. First, I had Spring MVC in my classpath, and Spring Cloud Gateway uses WebFlux. WebFlux recommends using WebClient over Feign. I decided to switch to WebClient.

I had to remove the following dependencies from my original

api-gateway
project:

implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

And add Spring Cloud Gateway with Resilience4j dependencies:

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

Then, I moved

CoolCarController
to its own class and re-implemented it with WebClient:

// api-gateway/src/main/java/com/example/apigateway/web/CoolCarController.java
@RestController
class CoolCarController {

    Logger log = LoggerFactory.getLogger(CoolCarController.class);

    private final WebClient.Builder webClientBuilder;
    private final ReactiveCircuitBreaker circuitBreaker;

    public CoolCarController(WebClient.Builder webClientBuilder,
                             ReactiveCircuitBreakerFactory circuitBreakerFactory) {
        this.webClientBuilder = webClientBuilder;
        this.circuitBreaker = circuitBreakerFactory.create("circuit-breaker");
    }

    record Car(String name) {
    }

    @GetMapping("/cool-cars")
    public Flux<Car> coolCars() {
        return circuitBreaker.run(
            webClientBuilder.build()
                .get().uri("http://car-service/cars")
                .retrieve().bodyToFlux(Car.class)
                .filter(this::isCool),
            throwable -> {
                log.warn("Error making request to car service", throwable);
                return Flux.empty();
            });
    }

    private boolean isCool(Car car) {
        return !car.name().equals("AMC Gremlin") &&
            !car.name().equals("Triumph Stag") &&
            !car.name().equals("Ford Pinto") &&
            !car.name().equals("Yugo GV");
    }
}

In order to inject the

WebClient.Builder
, I had to create a
WebClientConfiguration
class:

// api-gateway/src/main/java/com/example/apigateway/config/WebClientConfiguration.java
@Configuration
public class WebClientConfiguration {

    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

In the

car-service
project, I had to switch from using Spring Data REST to handling it with a
@RestController
and
@GetMapping
annotation. I removed the
@RepositoryRestResource
annotation from
CarRepository
and added a
CarController
class:

// car-service/src/main/java/com/example/carservice/web/CarController.java
@RestController
class CarController {

    private final CarRepository repository;

    public CarController(CarRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/cars")
    public List<Car> getCars() {
        return repository.findAll();
    }
}

NOTE: I did try to use Spring HATEOAS but ran into an issue when using it with the Okta Spring Boot starter.

To proxy

/home
to the downstream microservice, I added a
api-gateway/src/main/resources/application.yml
file to configure Spring Cloud Gateway to enable service discovery and specify routes:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: car-service
          uri: lb://car-service
          predicates:
            - Path=/home/**

At this point, I could access the car service directly at

http://localhost:8090/cars
and through the gateway at
http://localhost:8080/cool-cars
.

Secure Spring Boot Microservices with OAuth 2.0 and OIDC

To configure the Okta Spring Boot starter, there are a few properties in the

api-gateway
project's
application.properties
file:

okta.oauth2.issuer=${OKTA_OAUTH2_ISSUER}
okta.oauth2.client-id=${OKTA_OAUTH2_CLIENT_ID}
okta.oauth2.client-secret=${OKTA_OAUTH2_CLIENT_SECRET}
okta.oauth2.audience=${OKTA_OAUTH2_AUDIENCE}

The

car-service
is configured as an OAuth resource server and has the following properties in its
application.properties
file:

okta.oauth2.issuer=${OKTA_OAUTH2_ISSUER}
okta.oauth2.audience=${OKTA_OAUTH2_AUDIENCE}

The variables are read from the

.env
file in each project's root directory.

Fetch an Access Token as a JWT

When I first got things working, I was able to log in to the gateway, but when I tried to connect to the downstream microservice, it said the JWT was invalid. For this reason, I added a

/print-token
endpoint to the gateway that prints the access token to the console.

NOTE: The code in this section is for the WebFlux version. The MVC version is in the next section.

// api-gateway/src/main/java/com/example/apigateway/web/HomeController.java
@RestController
class HomeController {

    @GetMapping("/")
    public String howdy(@AuthenticationPrincipal OidcUser user) {
        return "Hello, " + user.getFullName();
    }

    @GetMapping("/print-token")
    public String printAccessToken(@RegisteredOAuth2AuthorizedClient("okta")
                                   OAuth2AuthorizedClient authorizedClient) {

        var accessToken = authorizedClient.getAccessToken();

        System.out.println("Access Token Value: " + accessToken.getTokenValue());
        System.out.println("Token Type: " + accessToken.getTokenType().getValue());
        System.out.println("Expires At: " + accessToken.getExpiresAt());

        return "Access token printed";
    }
}

Using jwt.io, I verified that it wasn't a valid JWT. I thought about trying to implement Spring Security's opaque token support, but discovered Auth0 doesn't have an

/instropection
endpoint. This makes it impossible to use opaque tokens with Auth0.

The good news is I figured out a workaround! If you pass a valid

audience
parameter to Auth0, you'll get a JWT for the access token. I logged an issue to improve the Okta Spring Boot starter and added a
SecurityConfiguration
class to solve the problem in the meantime.

// api-gateway/src/main/java/com/example/apigateway/config/SecurityConfiguration.java
@Configuration
public class SecurityConfiguration {

    @Value("${okta.oauth2.audience:}")
    private String audience;

    private final ReactiveClientRegistrationRepository clientRegistrationRepository;

    public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
        http
            .authorizeExchange(authz -> authz
                .anyExchange().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationRequestResolver(
                    authorizationRequestResolver(this.clientRegistrationRepository)
                )
            );
        return http.build();
    }

    private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver(
        ReactiveClientRegistrationRepository clientRegistrationRepository) {

        var authorizationRequestResolver =
            new DefaultServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
            authorizationRequestCustomizer());

        return authorizationRequestResolver;
    }

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
            .additionalParameters(params -> params.put("audience", audience));
    }
}

To make Spring Cloud Gateway pass the access token downstream, I added

TokenRelay
to its default filters in
application.yml
:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      default-filters:
        - TokenRelay
      routes: ...

I updated the

WebClientConfiguration
class to configure
WebClient
to include the access token with its requests:

// api-gateway/src/main/java/com/example/apigateway/config/WebClientConfiguration.java
@Configuration
public class WebClientConfiguration {

    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder(ReactiveClientRegistrationRepository clientRegistrations,
                                              ServerOAuth2AuthorizedClientRepository authorizedClients) {
        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
        oauth.setDefaultClientRegistrationId("okta");
        return WebClient
            .builder()
            .filter(oauth);
    }

}

Spring Cloud Gateway MVC and OAuth 2.0

To get the Spring Cloud Gateway MVC example working with OAuth 2.0, I had to add a

SecurityConfiguration
class to pass an
audience
parameter to Auth0:

// api-gateway/src/main/java/com/example/apigateway/config/SecurityConfiguration.java
@Configuration
public class SecurityConfiguration {

    @Value("${okta.oauth2.audience:}")
    private String audience;

    private final ClientRegistrationRepository clientRegistrationRepository;

    public SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization
                    .authorizationRequestResolver(
                        authorizationRequestResolver(this.clientRegistrationRepository)
                    )
                )
            );
        return http.build();
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
        ClientRegistrationRepository clientRegistrationRepository) {

        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
            new DefaultOAuth2AuthorizationRequestResolver(
                clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
            authorizationRequestCustomizer());

        return authorizationRequestResolver;
    }

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
            .additionalParameters(params -> params.put("audience", audience));
    }
}

Spring Cloud Gateway MVC 2023.0.0 doesn't allow you to configure a

TokenRelay
filter in YAML, so I added a
RouterFunction
bean to add it.

// api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
public class ApiGatewayApplication {

    @Bean
    public RouterFunction<ServerResponse> gatewayRouterFunctionsLoadBalancer() {
        return route("car-service")
            .route(path("/home/**"), http())
            .filter(lb("car-service"))
            .filter(tokenRelay())
            .build();
    }

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

Thanks for the help with this code, Spencer Gibb! 🙌

The updated

application.yml
file looks as follows after removing its
mvc
configuration.

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true

The last thing I needed to configure was OAuth integration for OpenFeign. I added the following properties to

application.properties
:

spring.cloud.openfeign.oauth2.enabled=true
spring.cloud.openfeign.oauth2.clientRegistrationId=okta

I removed the

spring.cloud.openfeign.circuitbreaker.enabled
property because I could not get it to work with Spring MVC. If you know how to make it work, please let me know in the comments!

Spring Boot Microservices and Refresh Tokens

In my previous Spring Boot 2.2 example, I couldn't get refresh tokens to work. I was able to get them to work this time! I changed the default scopes in

api-gateway
to request a refresh token using the
offline_access
scope:

# .env
OKTA_OAUTH2_AUDIENCE=https://fast-expiring-api
OKTA_OAUTH2_SCOPES=openid,profile,email,offline_access

And added a property to

application.properties
to read it:

# src/main/resources/application.properties
okta.oauth2.scopes=${OKTA_OAUTH2_SCOPES}

Then, I created an API in Auth0 called

fast-expiring-api
and set it to expire in 30 seconds:

auth0 apis create --name fast-expiring --identifier https://fast-expiring-api \
  --token-lifetime 30 --offline-access --no-input

If you do the same, you can restart the API gateway and go to

http://localhost:8080/print-token
to see your access token. You can copy the expired time to timestamp-converter.com to see when it expires in your local timezone. Wait 30 seconds and refresh the page. You'll see a request for a new token and an updated
Expires At
timestamp in your terminal.

The Okta Spring Boot starter and Keycloak

If you find yourself in a situation where you don't have an internet connection, it can be handy to run Keycloak locally in a Docker container. Since the Okta Spring Boot starter is a thin wrapper around Spring Security, it works with Keycloak, too.

In my experience, Spring Security's OAuth support works with any OAuth 2.0-compliant server. The Okta Spring Boot starter does validate the issuer to ensure it's an Okta URL, so you must use Spring Security's properties instead of the

okta.oauth2.*
properties when using Keycloak.

An easy way to get a pre-configured Keycloak instance is to use JHipster's

jhipster-sample-app-oauth2
application. It gets updated with every JHipster release. You can clone it with the following command:

git clone https://github.com/jhipster/jhipster-sample-app-oauth2.git --depth=1
cd jhipster-sample-app-oauth2

Then, start its Keycloak instance:

docker compose -f src/main/docker/keycloak.yml up -d

You can configure the

api-gateway
to use Keycloak by removing the
okta.oauth2.*
properties and using Spring Security's in
application.properties
:

spring.security.oauth2.client.provider.okta.issuer-uri=http://localhost:9080/realms/jhipster
spring.security.oauth2.client.registration.okta.client-id=web_app
spring.security.oauth2.client.registration.okta.client-secret=web_app
spring.security.oauth2.client.registration.okta.scope=openid,profile,email,offline_access

The

car-service
requires similar changes in its
application.properties
file:

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipster
spring.security.oauth2.resourceserver.jwt.audiences=account

Restart both apps, open

http://localhost:8080
, and you'll be able to log in with Keycloak:

Keycloack login page

Use

admin
/
admin
for credentials, and you can access
http://localhost:8080/cool-cars
as you did before:

Response from the cool-cars API

Stay secure with Spring Boot and Spring Cloud!

I hope you liked this tour of how to build Java microservice architectures with Spring Boot and Spring Cloud. You learned how to build everything with minimal code and then configure it to be secure with Spring Security, OAuth 2.0, OIDC, and Auth0 by Okta.

You can find all the code shown in this tutorial on GitHub in the @oktadev/auth0-java-microservices-examples repository. The OpenFeign example with Spring MVC is in the

spring-boot-gateway-mvc
directory. The Spring Cloud Gateway with WebFlux is in
spring-boot-gateway-webflux
. The Keycloak example is in the
keycloak
branch.

If you liked this post, you might enjoy these related posts:

We've also published some new labs about securing Spring Boot in our Developer Center. They're great if you like to learn by doing!

Please follow us on Twitter @oktadev and subscribe to our YouTube channel for more Spring Boot and microservices knowledge.

You can also sign up for our developer newsletter to stay updated on everything Identity and Security.