Build and Secure Spring Boot Microservices
Updated on April 17, 2024Introduction
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.
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.
Create Spring Boot Microservices
Create a directory to hold all your projects.
mkdir spring-boot-microservicescd 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
.
Run the below commands if you want to use Maven instead of Gradle.
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.
server.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=false
Add the @EnableEurekaServer
annotation to the DiscoveryServiceApplication
class.
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer@SpringBootApplicationpublic class DiscoveryServiceApplication { ... }
Start the discovery service application:
./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.
server.port=8090spring.application.name=car-servicespring.jpa.hibernate.ddl-auto=update
Create a Car
entity in the data
package with id
and name
properties.
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;@Entitypublic class Car {public Car() {}public Car(String name) {this.name = name;}@Id@GeneratedValueprivate Long id;@NotNullprivate 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:
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.
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@SpringBootApplicationpublic class CarServiceApplication {public static void main(String[] args) {SpringApplication.run(CarServiceApplication.class, args);}@BeanApplicationRunner 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.
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;@RestControllerclass 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.
services:postgres:image: 'postgres:latest'environment:- 'POSTGRES_DB=mydatabase'- 'POSTGRES_PASSWORD=secret'- 'POSTGRES_USER=myuser'ports:- '5432'
Start the car service application:
./gradlew bootRun
Confirm you can access the /cars
endpoint:
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.
spring:application:name: api-gatewaycloud:gateway:discovery:locator:enabled: true
Update ApiGatewayApplication.java
to enable service discovery:
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@EnableDiscoveryClient@SpringBootApplicationpublic class ApiGatewayApplication { ... }
Add a WebClientConfiguration
class in the config
package to configure a WebClient
.
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;@Configurationpublic class WebClientConfiguration {@Bean@LoadBalancedpublic 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.
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;@RestControllerclass 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:
./gradlew bootRun
Confirm you can access the /cool-cars
endpoint:
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:
implementation 'com.okta.spring:okta-spring-boot-starter:3.0.6'
If you're using Maven, update your 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:
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.
Now, create an api-gateway/application.properties
file to configure the Okta Spring Boot starter:
# trailing slash is important for issuerokta.oauth2.issuer=https://AUTH0-DOMAIN/okta.oauth2.client-id=AUTH0-CLIENT-IDokta.oauth2.client-secret=AUTH0-CLIENT-SECRETokta.oauth2.audience=${okta.oauth2.issuer}api/v2/
Add this file to api-gateway/.gitignore
so you don't accidentally check it into source control:
application.properties
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.
# trailing slash is important for issuerokta.oauth2.issuer=https://AUTH0-DOMAIN/okta.oauth2.audience=${okta.oauth2.issuer}api/v2/
Add a HomeController
class to the car service projects web
package that displays the access token's claims.
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;@RestControllerpublic 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.
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;@RestControllerclass 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.
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;@Configurationpublic class WebClientConfiguration {@Bean@LoadBalancedpublic 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.
spring:application:name: api-gatewaycloud:gateway:discovery:locator:enabled: truedefault-filters:- TokenRelayroutes:- id: car-serviceuri: lb://car-servicepredicates:- 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.
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;@Configurationpublic class SecurityConfiguration {@Value("${okta.oauth2.audience:}")private String audience;private final ReactiveClientRegistrationRepository clientRegistrationRepository;public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {this.clientRegistrationRepository = clientRegistrationRepository;}@Beanpublic 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:
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.
...okta.oauth2.audience=https://fast-expiring-apiokta.oauth2.scopes=openid,profile,email,offline_accesslogging.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.
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.
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:
git clone https://github.com/jhipster/jhipster-sample-app-oauth2.git --depth=1cd jhipster-sample-app-oauth2
Start Keycloak with Docker Compose:
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
:
spring.security.oauth2.client.provider.okta.issuer-uri=http://localhost:9080/realms/jhipsterspring.security.oauth2.client.registration.okta.client-id=web_appspring.security.oauth2.client.registration.okta.client-secret=web_appspring.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
:
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipsterspring.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-secretsauth0 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.