Spring Boot is a very popular framework for creating microservices quickly and easily.
In this demo, I’ll show you how to:
-
Create a central configuration server using Spring Boot.
-
Create two separate microservices/applications using Spring Boot, which read their configuration from the server.
-
Secure the applications using OAuth 2.0 and Okta.
-
Demonstrate how to (securely) refresh a service’s configuration without redeploying.
Prerequisites:
- Create a Spring Cloud Config Server
- Create an OpenID Connect Application
- Configure Security for Your Microservices Architecture
- Create Spring Boot Microservice #1
- Refresh the Configuration in Your Spring Cloud Config Server
- Create Spring Boot Microservice #2
- Learn More About Spring Cloud Config and Microservices
You can create a Spring Cloud Config Server using Spring Initialzr.
-
Click this link or go to start.spring.io and select the following options in your browser:
-
Group:
com.okta.dev
-
Artifact:
config-server
-
Name:
cloud-config-server
-
Description: Configuration Server
-
Package:
com.okta.dev.configserver
-
Dependencies:
Spring Security
,Spring Web
,Config Server
-
-
Click Generate to download the project files. Unzip the file and import the project files into your favorite IDE.
-
Update
src/main/resources/application.properties
to have the following key-value pairs:server.port=8888 spring.cloud.config.server.native.search-locations=file://${user.home}/config spring.security.user.name=configUser spring.security.user.password=configPass
-
Open your application’s main class and add the
@EnableConfigServer
annotation:import org.springframework.cloud.config.server.EnableConfigServer; @EnableConfigServer @SpringBootApplication public class CloudConfigServerApplication { ... }
-
Use the Okta CLI to create a new OIDC app:
okta apps create
-
Select Web > Other. Then, change the Redirect URI to:
http://localhost:8001/login/oauth2/code/okta,http://localhost:8002/login/oauth2/code/okta
-
Accept the defaults for the Logout Redirect URIs.
In the search-locations
directory, add several files:
server:
port: 8001
okta:
oauth2:
issuer: https://YOUR_OKTA_DOMAIN/oauth2/default
clientId: YOUR_CLIENT_ID
clientSecret: YOUR_CLIENT_SECRET
hello:
message: "Service One Profile One"
hello:
message: "Service One Profile Two"
server:
port: 8002
okta:
oauth2:
issuer: https://YOUR_OKTA_DOMAIN/oauth2/default
clientId: YOUR_CLIENT_ID
clientSecret: YOUR_CLIENT_SECRET
hello:
message: "Service Two Profile One"
hello:
message: "Service Two Profile Two"
The filenames are important and must be in a certain pattern for your microservices to pick them up:
/{application}/{profile}[/{label}] /{application}-{profile}.yml /{label}/{application}-{profile}.yml /{application}-{profile}.properties /{label}/{application}-{profile}.properties
Where:
-
{application}
is the name of your microservice specified via your microservice’sspring.application.name
property. In this case,service-one
andservice-two
. -
{profile}
matches the list of profiles your microservice is running via thespring.profiles.active
property. In this case,profile1
andprofile2
. -
{label}
is an additional descriptor usually corresponding to a version control branch, e.g.dev
orstg
. It can be manually set via thespring.cloud.config.label
property in the microservice’sbootstrap.properties
file or set on the command line (-Dspring.cloud.config.label
).
Enter your config server’s project directory and run the application:
./mvnw spring-boot:run -Dspring-boot.run.profiles=native
The native
profile tells the application to serve configuration files from the filesystem directory you populated above.
-
Open the Spring Initializr and select the following options or click here.
-
Group:
com.okta.dev
-
Artifact:
service-one
-
Name:
service-one
-
Description: Microservice One
-
Package:
com.okta.dev.service-one
-
Dependencies:
Spring Web
,Okta
,Config Client
,Actuator
-
-
Click Generate and import the project files into your favorite IDE.
-
Update
src/main/resources/application.properties
with the following key-value pairs:spring.application.name=service-one spring.config.import=configserver: spring.cloud.config.uri=http://localhost:8888 spring.cloud.config.username=configUser spring.cloud.config.password=configPass
-
To secure your microservice using Okta and OAuth 2.0, open your microservice’s main class and add the following configuration class:
@Configuration public static class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login(); } }
-
Add a basic REST controller, which will respond with a message defined in your service’s configuration file (hosted on the config server):
@RestController @RequestMapping("/secure") public static class SecureController { @Value("${hello.message}") private String helloMessage; @GetMapping public String secure(Principal principal) { return helloMessage; } }
-
Enter your config server’s project directory and run the application with
profile1
set:cd /path/to/service-one ./mvnw spring-boot:run -Dspring-boot.run.profiles=profile1
-
Open a browser and navigate to
http://localhost:8001/secure
. After successfully authenticating, you should see the following message:Service One Profile One
This is the same message defined in the
service-one-profile.yml
file you created earlier. Neat! -
Switch your microservice’s active profile to
profile2
and observe a different message. Stop your application and re-run withprofile2
active:./mvnw spring-boot:run -Dspring-boot.run.profiles=profile2
-
Navigate to
http://localhost:8001/secure
. You should now see the message defined inservice-one-profile2.yml
:Service One Profile Two
Spring Cloud Config provides the ability to "live" reload your service’s configuration without stopping or re-deploying.
-
Stop
service-one
and add the@RefreshScope
annotation to your REST controller:import org.springframework.cloud.context.config.annotation.RefreshScope; ... @RefreshScope @RestController @RequestMapping("/secure") public static class SecureController { ... }
When this annotation is applied to a Spring component (i.e., a
@Component
,@Service
,@RestController
, etc.), the component is re-created when a configuration refresh occurs, in this case giving an updated value for${hello.message}
.You can refresh an application’s configuration by including the Spring Boot Actuator dependency, exposing the
/actuator/refresh
endpoint, and sending an emptyPOST
request. -
The Spring Boot Actuator has already been included in your microservice’s dependencies. Edit your configuration files to expose the
refresh
endpoint:service-one.ymlserver: port: 8001 okta: oauth2: issuer: https://YOUR_OKTA_DOMAIN/oauth2/default clientId: YOUR_CLIENT_ID clientSecret: YOUR_CLIENT_SECRET management: endpoints: web: exposure: include: "refresh"
service-two.ymlserver: port: 8002 okta: oauth2: issuer: https://YOUR_OKTA_DOMAIN/oauth2/default clientId: YOUR_CLIENT_ID clientSecret: YOUR_CLIENT_SECRET management: endpoints: web: exposure: include: "refresh"
-
Add a security class inside your main application class to secure the endpoint with basic authentication:
@Configuration public static class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .antMatcher("/actuator/*") .authorizeRequests() .antMatchers("/actuator/*").authenticated() .and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("serviceOneUser") .password("{noop}serviceOnePassword") .roles("USER"); } }
Almost finished! Since your application is already authenticated with OIDC using Okta, you need to make these two security configuration classes play nicely with each other.
-
Add the
@Order
annotations to both soActuatorSecurityConfig
takes precedence. This will allow you to refresh the configuration via/actuator/refresh
without triggering the OAuth 2.0 flow.@Order(1) @Configuration public static class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter { ... } @Order(2) @Configuration public static class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { ... }
-
Start your application using
profile1
:./mvnw spring-boot:run -Dspring-boot.run.profiles=profile1
-
Navigate to
http://localhost:8001/secure
and note the message still saysService One Profile One
. -
Open your configuration file at
/path/to/config/folder/service-one-profile1.yml
and edit the message:service-one-profile1.ymlhello: message: "Things have changed"
-
Save the file and refresh the page at
http://localhost:8001/secure
. Note that the message has not changed yet and still saysService One Profile One
. To have your application receive the updated configuration, you must call the/actuator/refresh
endpoint:curl -u serviceOneUser:serviceOnePassword -X POST http://localhost:8001/actuator/refresh
-
Refresh the page at
http://localhost:8001/secure
, and you should see the updated message!
Create a second Spring Boot application, acting as a second microservice, which will also have its configuration provided by your configuration server.
-
Use the Spring Initializr with the following options or click this link.
-
Group:
com.okta.dev
-
Artifact:
service-two
-
Name:
service-two
-
Description: Microservice Two
-
Package:
com.okta.dev.service-two
-
Dependencies:
Spring Web
,Okta
,Config Client
,Actuator
-
-
Click Generate and import the project files into your favorite IDE.
-
Update
src/main/resources/application.properties
with the following properties:spring.application.name=service-two spring.config.import=configserver: spring.cloud.config.uri=http://localhost:8888 spring.cloud.config.username=configUser spring.cloud.config.password=configPass
Note the value for
spring.application.name
is different. -
Make the same changes to your main application class as above:
public class ServiceTwoApplication { @Order(1) @Configuration public static class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { ... } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("serviceTwoUser") .password("{noop}serviceTwoPassword") .roles("USER"); } } @Order(2) @Configuration public static class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { ... } @RefreshScope @RestController @RequestMapping("/secure") public static class SecureController { @Value("${hello.message}") private String helloMessage; @GetMapping public String secure(Principal principal) { return helloMessage; } } }
Notice the different credentials for the in-memory user:
serviceTwoUser / serviceTwoPassword
. -
Run the application:
cd /path/to/service-two ./mvnw spring-boot:run -Dspring-boot.run.profiles=profile1
-
Navigate to
http://localhost:8002/secure
and authenticate with Okta. When you are redirected back to your application you will see the welcome message forservice-two
:Service Two Profile One
You’re done! You’ve created two microservices, secured by Okta and OAuth 2.0, which receive their configuration settings from a shared Spring Cloud Config server. Very cool! 😎
⚙️ Find the code on GitHub: @oktadev/okta-spring-cloud-config-example.
👀 Read the blog post: Spring Cloud Config for Shared Microservice Configuration.