spring logo
java logo

Build and Secure Spring Boot Microservices

Updated on April 17, 2024
Photo of Matt Raible
Matt RaibleDeveloper Advocate

Introduction

Learning GoalLearn how to build microservices with Spring Boot and Spring Cloud and secure communication between them.

In this guide, you will learn:

  • How to build a Spring Boot microservice with Java.
  • How to build an API gateway with Spring Cloud Gateway.
  • How to secure your microservices architecture with OAuth and OpenID Connect.
  • How to use refresh token for enhanced security.
  • How to use the Okta Spring Boot Starter with Keycloak.

Why Use Spring to Build Microservices?

Spring Boot is one of the most popular frameworks for developing Java applications. Spring Cloud is a companion project that allows you to build microservices quickly using the Spring framework. It provides a set of tools for common patterns in distributed systems, such as configuration management, service discovery, circuit breakers, intelligent routing, and more.

The OAuth 2.0 authorization framework is a protocol that allows a user to grant a third-party website or application access to the user's protected resources without necessarily revealing their long-term credentials or even their identity.

OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 framework. It allows third-party applications to verify the identity of the end-user and to obtain basic user profile information.

In this guide, you'll learn how to use Spring Boot and Spring Cloud to build a simple microservice architecture that's secured with OpenID Connect (OIDC) and OAuth 2.0. You'll also learn how to use refresh tokens and how to use the Okta Spring Boot Starter with Keycloak.

Set up a Development Environment

  • Use your favorite text editor or IDE. We recommend using IntelliJ IDEA.
  • Ensure that you have Java 17+ installed in your system. You can easily install it using SDKMAN!.
  • Ensure you have Docker and Docker Compose installed.
  • Windows commands in this guide are written for PowerShell.

Create an Auth0 Account

If you already have an Auth0 account, you can log in to your tenant and continue to the next step.

Otherwise, sign up for a free Auth0 account.

During the sign-up process, you create something called an Auth0 Tenant, where you configure your use of Auth0.

Once you sign in, Auth0 takes you to the Auth0 Dashboard, where you can configure and manage Auth0 assets, such as applications, APIs, connections, and user profiles.

Set up the Auth0 CLI

If you are not familiar with the Auth0 CLI, you can follow the "Auth0 CLI Basics" lab to learn how to build, manage, and test your Auth0 integrations from the command line.

There are different ways to install the Auth0 CLI, depending on your operating system.

LOADING...

Create Spring Boot Microservices

Create a directory to hold all your projects.

COMMAND
mkdir spring-boot-microservices
cd spring-boot-microservices

Create three projects using start.spring.io's REST API:

  • discovery-service: a Netflix Eureka server used for service discovery. Dependencies used: Eureka Server.
  • car-service: a simple Car Service that uses Spring Data REST to serve up a REST API of cars. Dependencies used: Spring Boot Actuator, Eureka Discovery Client, Spring Data JPA, Rest Repositories, PostgreSQL Driver, Spring Web, Validation, Spring Boot DevTools, and Docker Compose Support.
  • 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). Dependencies used: Eureka Discovery Client, Reactive Gateway, and Resilience4J.

This guide is using Spring Boot version 3.2.4.

LOADING...

Run the below commands if you want to use Maven instead of Gradle.

LOADING...

Add Service Discovery with Netflix Eureka

In the discovery-service project, configure the application.properties file to use port 8761 and turn off registration with Eureka.

discovery-service/src/main/resources/application.properties
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Add the @EnableEurekaServer annotation to the DiscoveryServiceApplication class.

discovery-service/src/main/java/com/example/discoveryservice/DiscoveryServiceApplication.java
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServiceApplication { ... }

Start the discovery service application:

GRADLE
MAVEN
./gradlew bootRun

You can access the Eureka instance at http://localhost:8761/.

Build a Java Microservice with Spring Data REST

In the car-service project, configure the application.properties file to use port 8090, to have an application name, and to create the database automatically.

car-service/src/main/resources/application.properties
server.port=8090
spring.application.name=car-service
spring.jpa.hibernate.ddl-auto=update

Create a Car entity in the data package with id and name properties.

car-service/src/main/java/com/example/carservice/data/Car.java
package com.example.carservice.data;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import java.util.Objects;
@Entity
public class Car {
public Car() {
}
public Car(String name) {
this.name = name;
}
@Id
@GeneratedValue
private Long id;
@NotNull
private String name;
// generate getters and setters with your IDE
// create equals(), hashCode(), and toString() with your IDE
}

Don't forget to generate getters, setters, toString(), equals(), and hashCode() methods using your IDE.

Create a CarRepository interface in the same package:

car-service/src/main/java/com/example/carservice/data/CarRepository.java
package com.example.carservice.data;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CarRepository extends JpaRepository<Car, Long> {
}

Modify CarServiceApplication to enable service discovery and to create a default set of cars when the application loads.

car-service/src/main/java/com/example/carservice/CarServiceApplication.java
package com.example.carservice;
import com.example.carservice.data.Car;
import com.example.carservice.data.CarRepository;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import java.util.stream.Stream;
@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);
};
}
}

Create a CarController class in the web package to expose a /cars endpoint.

car-service/src/main/java/com/example/carservice/web/CarController.java
package com.example.carservice.web;
import com.example.carservice.data.Car;
import com.example.carservice.data.CarRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
class CarController {
private final CarRepository repository;
public CarController(CarRepository repository) {
this.repository = repository;
}
@GetMapping("/cars")
public List<Car> getCars() {
return repository.findAll();
}
}

There's a compose.yaml file in the root directory to start a PostgreSQL instance. This will be automatically started when you start the application.

car-service/compose.yaml
services:
postgres:
image: 'postgres:latest'
environment:
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
- 'POSTGRES_USER=myuser'
ports:
- '5432'

Start the car service application:

GRADLE
MAVEN
./gradlew bootRun

Confirm you can access the /cars endpoint:

LOADING...

Add Routing with Spring Cloud Gateway

In the api-gateway project, rename the application.properties file to application.yml and enable service discovery. Using YAML makes further configuration easier.

api-gateway/src/main/resources/application.yml
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true

Update ApiGatewayApplication.java to enable service discovery:

api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication { ... }

Add a WebClientConfiguration class in the config package to configure a WebClient.

api-gateway/src/main/java/com/example/apigateway/config/WebClientConfiguration.java
package com.example.apigateway.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfiguration {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}

Add a CoolCarController in the web package to fetch and filter cars from the car microservice. Notice how the ReactiveCircuitBreaker is used to run the WebClient call in a circuit breaker.

api-gateway/src/main/java/com/example/apigateway/web/CoolCarController.java
package com.example.apigateway.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
@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");
}
}

Start the API gateway application:

GRADLE
MAVEN
./gradlew bootRun

Confirm you can access the /cool-cars endpoint:

LOADING...

Secure Spring Boot Microservices with OAuth 2.0 and OIDC

To secure your microservices, you'll use OAuth 2.0 and OpenID Connect (OIDC) with Auth0. Auth0 is a popular identity provider that supports many different authentication and authorization protocols. It's easy to use and has a generous free tier.

Modify the build.gradle files in both the api-gateway and car-service projects to use the Okta Spring Boot starter:

build.gradle
implementation 'com.okta.spring:okta-spring-boot-starter:3.0.6'

If you're using Maven, update your pom.xml:

pom.xml
<dependency>
<groupId>com.okta.spring</groupId>
<artifactId>okta-spring-boot-starter</artifactId>
<version>3.0.6</version>
</dependency>

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 OIDC app with the appropriate URLs:

LOADING...

Copy the domain, client ID, and client secret of your app and paste it into the following input boxes:

When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes copying and pasting code as you follow along easy.

For security, these configuration values are stored in memory and only used locally. They are gone as soon as you refresh the page!

Now, create an api-gateway/application.properties file to configure the Okta Spring Boot starter:

api-gateway/application.properties
# trailing slash is important for issuer
okta.oauth2.issuer=https://AUTH0-DOMAIN/
okta.oauth2.client-id=AUTH0-CLIENT-ID
okta.oauth2.client-secret=AUTH0-CLIENT-SECRET
okta.oauth2.audience=${okta.oauth2.issuer}api/v2/

Add this file to api-gateway/.gitignore so you don't accidentally check it into source control:

api-gateway/.gitignore
application.properties
You can also put these values in api-gateway/src/main/resources/application.yml. However, we recommend you DO NOT include the client secret in this file for security reasons.

Create car-service/application.properties and add the below configuration.

car-service/application.properties
# trailing slash is important for issuer
okta.oauth2.issuer=https://AUTH0-DOMAIN/
okta.oauth2.audience=${okta.oauth2.issuer}api/v2/
The car service doesn't need the client ID and secret because it's acting as a resource server and simply validates the access token, without communicating with Auth0.

Add a HomeController class to the car service projects web package that displays the access token's claims.

car-service/src/main/java/com/example/carservice/web/HomeController.java
package com.example.carservice.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
public class HomeController {
private final static Logger log = LoggerFactory.getLogger(HomeController.class);
@GetMapping("/home")
public String home(Principal principal) {
var username = principal.getName();
if (principal instanceof JwtAuthenticationToken token) {
log.info("claims: " + token.getTokenAttributes());
}
return "Hello, " + username;
}
}

Add a HomeController class to the API gateway projects web package that displays your user's name and access token.

api-gateway/src/main/java/com/example/apigateway/web/HomeController.java
package com.example.apigateway.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@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";
}
}

Update the WebClientConfiguration class in the API gateway to configure WebClient to send the access token in an Authorization header.

api-gateway/src/main/java/com/example/apigateway/config/WebClientConfiguration.java
package com.example.apigateway.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.web.reactive.function.client.WebClient;
@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);
}
}

Update gateway's application.yml to use a TokenRelayFilter to forward the access token to the car service and map /home to the car service's /home endpoint.

api-gateway/src/main/resources/application.yml
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
default-filters:
- TokenRelay
routes:
- id: car-service
uri: lb://car-service
predicates:
- Path=/home/**

Restart both the car service and API gateway applications using Ctrl+C and ./gradlew bootRun.

Open http://localhost:8080 in your favorite browser. You'll be redirected to Auth0 to log in. After authenticating, you'll see your name in lights! ✨

If you go to http://localhost:8080/cool-cars, you won't see any data and there will be an error in your gateway app's console.

401 UNAUTHORIZED from GET http://car-service/cars [DefaultWebClient]

Go to http://localhost:8080/print-token and view the access token printed to the console.

Check if it's a valid access token by copying/pasting it into jwt.io or JWT UI. You'll see it's invalid. This is because Auth0 returns an opaque token when you don't pass in an audience parameter.

Fetch an Access Token as a JWT

Create a SecurityConfiguration.java class in the API gateway projects config package to configure Spring Security to send an audience parameter to Auth0.

api-gateway/src/main/java/com/example/apigateway/config/SecurityConfiguration.java
package com.example.apigateway.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.web.server.SecurityWebFilterChain;
import java.util.function.Consumer;
@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));
}
}

Restart the API gateway and now http://localhost:8080/print-token will print a valid JWT. Check if the other URLs work:

Copy the JWT from the console and access the car service directly.

Paste the access token value in the following field so that you can use it to test your resource server:

LOADING...

Use Refresh Tokens for Enhanced Security

Change the default scopes in the gateway project to request a refresh token using the offline_access scope. Also, change the audience to be one that quickly expires its access tokens. Add logging too.

api-gateway/application.properties
...
okta.oauth2.audience=https://fast-expiring-api
okta.oauth2.scopes=openid,profile,email,offline_access
logging.level.org.springframework.web.reactive.function.client=DEBUG

Create a new API in Auth0 and configure it to have a 30-second access token lifetime.

LOADING...

Restart the API gateway and go to http://localhost:8080/print-token to see your access token.

Copy the expired time to timestamp-converter.com (under ISO 8601) 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.

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. Clone it with the following command:

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

Start Keycloak with Docker Compose:

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

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

api-gateway/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

Update the car service to use Keycloak by removing the okta.oauth2.* properties and using Spring Security's in application.properties:

car-service/application.properties
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.

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

Recap

In this guide, you learned how to build secure Java microservices with Spring Boot and Spring Cloud. You also learned how to use refresh tokens and the Okta Spring Boot Starter with Keycloak. It's pretty cool how you can configure Spring Boot microservices with Auth0 so quickly!

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
auth0 apis create --name fast-expiring --identifier https://fast-expiring-api \
--token-lifetime 30 --offline-access --no-input

Check out our other Spring Boot guides Authentication in Spring Boot, Authorization in Spring Boot and Role Based Access Control in Spring Boot to learn more about Auth0 security integration in Spring Boot Java applications.

Be sure to visit the Okta Spring Boot Starter's GitHub repository to stay informed on the latest developments and join the growing community of Spring Boot users who use Okta.