Implement an API Gateway and a Service Discovery Server using Springboot, Spring Security, Keycloak, Oauth2 and Netflix Eureka today
How we can do Service Discovery and Spring Security these days 🤔🤔
Requirements
Knowledge of Java and Springboot . Also have a Docker installation on your system
Note: If you are a speedster and just want a link to the code then I got you github repo.
We would use the dependency manager called Maven and it’s multi module project setup in this demonstration.
Go into your IDE of choice (using Intellij Idea here) and create a new springboot project or you can use the Spring Initializer page. Name the project however you want. In your IDE right click on the root folder and create two modules one named discovery-server and the other named api-gateway. So by now the current structure is that we have a parent springboot project named whatever name you gave it, then two child springboot projects named discovery-server and api-gateway respectively.
Editing the Pom.xml files
Some housekeeping; We would edit the pom.xml files for the parent project and the two child modules. Add this just before the <modelVersion>4.0.0</modelVersion> in the parent project’s pom.xml file
parentProject/pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
This line here ensures that springboot starter parent is the parent of the parent project 😂
To be sure you did your project structure right, this code below should already be in your parent project’s pom.xml file
<modules>
<module>discovery-server</module>
<module>api-gateway</module>
</modules>
That above shows that indeed there are two child modules.
Add this line to the <properties></properties> section in the pom.xml
<properties>
// ... some code already there
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
Still on the parent project’s pom/xml file we now add a dependency management section to it with code below
<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>
What we added above is for the two child modules so we avoid repitition.
Next we head to the discovery-server’s pom.xml file
If you did your project structure right, the pom.xml file of both the discovery-server and the api-gateway should contain this
<parent>
<groupId>com.starq</groupId>
<artifactId>spring-sec-microservice-medium-blog</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
The code above shows that they are both child modules of the parent project.
Now add this dependency to the discovery-server’s pom.xml file
discovery-server/pom.xml
<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>
</dependencies>
If you notice, in the code above, we have added Netflix Eureka Server . We need this for the micro services to be able to discover each other and then communicate with each other.
Lastly we add this to the api-gateway’s pom.xml file
api-gateway/pom.xml
<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-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
If you are using Intellij Idea then use the maven icon to install all the dependencies or you can navigate to the parent project directory and run this command to install everything for both child modules
mvn clean install
Setting up the keycloak server
Keycloak is an identity access management server that our OAuth2 authorization feature is going to run on.
We would use a docker image to run the keycloak server. Here’s the docker-compose file for the image and preferably keep this file in the root folder of your api-gateway service
version: '3.8'
services:
keycloak:
container_name: keycloak
image: codingpuss/keycloak-silicon:16.1.1
restart: always
environment:
- KEYCLOAK_USER=admin
- KEYCLOAK_PASSWORD=admin
- KEYCLOAK_EXTRA_ARGS="-Dkeycloak.profile.feature.upload_scripts=enabled"
ports:
- "8181:8080"
depends_on:
- keycloak-db
keycloak-db:
image: postgres
environment:
- POSTGRES_DB=keycloak
- POSTGRES_USER=keycloak
- POSTGRES_PASSWORD=password
I use an apple silicon chip hence the keycloak image I used but you can always use the official one.
Run the docker command below to install it, get it running and detach it.
docker compose up -d
The keycloak server should be running on http://localhost:8181. It should show you an administrator log in screen. Put in the username and password in the docker-compose file which is admin and admin respectively and sign in. It should ask you to change the password afterward.
Next we need to create something called a realm which is basically a grouping for our oauth2 clients. On the top left side of the dashboard you should see a drop down labelled Master, hover on it and a button named add realm should appear, click on it. Give a name to the realm, I used oauth2-eureka-tutorial and then click the create button.
After that is done, the navigation highlight should be on Realm Settings on the left side panel under the Configure section. Click on Clients which should be directly under Realm Settings. It should take you to the clients page which has a create button on the top far right. Click on it and it should take you to a screen where all you need to do is provide the Client ID. We can use spring-eureka-oauth2-client for it and click save afterwards.
By now you should be shown a screen for further settings. Scroll down to where you have Access Type and change it from Public to Confidential.
Disable Standard Flow Enabled and Direct Access Grants Enabled then also enable the Service Accounts Enabled. You can go right ahead and click the save button at the buttom of the screen. After it saves still on that screen there are some tabs on top of the current view and you should be on the Settings tab. Click on the Credentials tab, copy the Secret and save it somewhere because when you want to make HTTP requests to any of your other services that require authorization maybe using Postman or any frontend web application, you would need that secret in your oauth2 request setup.
Lastly go back to the Realm Settings on the left side panel, under the Endpoints input, click on the Open ID Endpoint Configuration link. It should load a new page with JSON data. The first key in the JSON data should be issuer, copy the value which should be a http link and save it somewhere also.
Configuration Time 🤓🤓🤓
We need to add some configuration settings to the application.properties file of both the discovery-server and api-gateway services.
If you didn’t use Spring Initializer then go to your resources folder in /src/main/, inside the resources folder create a file named application.properties, do this in both the discovery-server and api-gateway services.
Discovery-Server Application.properties
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
server.port=8761
## here below we are using an environmental variable in our properties file
eureka.username=${EUREKA_USERNAME:eureka}
eureka.password=${EUREKA_PASSWORD:password}
In the code above, we did some configuration setup for the Eureka server.
Also the environmental variable username and password setting there is for an in-memory authentication between any other microservice in your application that is not the discovery-server.
API-Gateway Application.properties
eureka.client.service-url.defaultZone=http://eureka:password@localhost:8761/eureka
spring.application.name=api-gateway
logging.level.root=INFO
logging.level.org.springframework.cloud.gateway.route.RouteDefinitionLocator=INFO
logging.level.org.springframework.cloud.gateway=TRACE1
## Discover server route
spring.cloud.gateway.routes[0].id=discovery-server
spring.cloud.gateway.routes[0].uri=http://localhost:8761
spring.cloud.gateway.routes[0].predicates[0]=Path=/eureka/web
# so we access the discovery server through http://localhost:8080/eureka/web
spring.cloud.gateway.routes[0].filters[0]=SetPath=/
#the filter defined above is to reroute the request from http://localhost:8080/eureka/web to http://localhost:8761
# which is really where our eureka discovery server resides
#Discovery server static resources
spring.cloud.gateway.routes[1].id=discovery-server-static
spring.cloud.gateway.routes[1].uri=http://localhost:8761
spring.cloud.gateway.routes[1].predicates[0]=Path=/eureka/**
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8281/auth/realms/oauth2-eureka-tutorial
Above we set the URL of the Eureka service discovery server to establish communication with our api-gateway
Next we register routes for the Eureka discovery server and also it’s frontend assets.
Also on the last line replace the issuer-uri with the one you copied from your realm configuration
You can also register routes for any other microservice in your application in the above file.
The Code 😎
Ensure both child services are springboot applications
If you used Spring Initializer you can skip this next step
For the api-gateway service, delete the Main class and create this new class file apigateway.ApiGatewayApplication.java. That should first create a package named apigateway then a java class named then ApiGatewayApplication.java inside it. Now add this code
package com.starq.apigateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args){
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
Nothing much here as we only just converted this api-gateway service to a springboot application. However notice we added the annotation @EnableDiscoveryClient. It used to be @EnableEurekaClient but that’s deprecated. This annotation helps your service automatically connect to the discovery-server as a client.
We do the same for the discovery-server service. Delete the Main class file and create this in the same directory; discoveryserver.DiscoveryServerApplication.java. Add the code below to it
package com.starq.discoveryserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args){
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
Little to discuss on the above code as we have equally converted the discovery-server to a springboot application and also used the @EnableEurekaServer annotation to make this service a Eureka Service Discovery Server.
Lastly the Security Config
We need to do some further security configurations so our services can work as intended. This section is the reason I did this write up and that’s because Springboot has done some upgrade on the way you implement these features therefore some deprecations. Let’s start with the discover-server.
Discovery-server security config
In your discoveryserver package folder create a class this way: config.SecurityConfig. Then populate it with this code below
package com.starq.discoveryserver.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.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${eureka.username}")
private String username;
@Value("${eureka.password}")
private String password;
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username(username)
.password(password)
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
Above we use a @Configuration annotation because we want the springboot application context to have access to these as the application begins.
Next the @EnableWebSecurity gives us access to Spring Security’s web security features so we can customize it to fit our needs
We injected the username and password env variables into their counterpart String variables for use in this class and by now you should already know we are implementing a single user authentication.
The first method userDetailsService creates a InMemoryUserDetailsManager bean, which is a simple implementation of UserDetailsService that stores user details in memory.
The second method creates a SecurityFilterChain bean basically represents the security filter chain for HTTP requests.
We further disable CSRF protection, authorize all HTTP requests but require authentication, configure a basic HTTP authentication and then return SecurityFilterChain bean.
Api-gateway security config
Now to your apigateway package folder and create this class: config.SecurityConfig and add this code to it
package com.starq.apigateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity serverHttpSecurity){
serverHttpSecurity
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchange-> exchange
.pathMatchers("/eureka/**")
.permitAll()
.anyExchange()
.authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return serverHttpSecurity.build();
}
}
We enable @EnableWebFluxSecurity annotation gives us access to Spring Security’s web security features for a reactive application. This application is reactive because your api-gateway is also a service that needs to be discovered and because of the dynamism of web applications where ports and host urls can change, the discovery server needs to be updated everytime something about the url or port of the api-gateway or any other microservice in the entirety of your application changes.
The only method here creates a SecurityFilterChain bean which represents the security filter chain for reactive applications.
We disable CSRF protection and ensure every request goes through authorization except one with path “/eureka/”.
We then configure oauth2 server settings specifically with JWT authentication; This is where you would need the secret you saved earlier on for testing authorization on any other of your microservices.
Finally we return the SecurityWebFilterChain bean.
Testing Tasting 😋😋😋
All that is left to do is test. Still having your docker keycloak container running, run both the discovery-server and the api-gateway services. You would normally click on the play button on the class definition of each of the services springboot application files if you are using Intellij Idea IDEi.e DiscoveryServerApplication and ApiGatewayApplication or you can navigate to the root folders of the discovery-server and the api-gateway and run this command in each
mvn spring-boot:run
Navigate to http://localhost:8080/eureka/web.
This should load a page asking for password and username, type in eureka and password as we already set for the single user in memory authentication. Afterwards it should log you into the eureka dashboard. This shows that the api-gateway routing works because the eureka server’s port is 8761 but here we used 8080 which is the default port for the api-gateway and yet it still rerouted us properly. Note that even if you navigated to http://localhost:8761, it would still prompt you for the username and password.
Secondly on the loaded eureka server dashboard you should be able to see the api-gateway’s instance information showing that it’s part of the service discovery.
Conclusion
Springboot has really done a good job in making integration of security to your applications easy and seamless. You can go right ahead and add one more microservice to this application maybe a product-service. The only thing you need to do is to add this to your pom.xml file to install the netflix-eureka client dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Also add this annotation @EnableDiscoveryClient to the springboot application entry point and finally add this to your application.properties file
eureka.client.service-url.defaultZone=http://eureka:password@localhost:8761/eureka
spring.application.name=product-service
server.port=0
We ensured a client to server connection to the discovery-server and we assigned a port 0 for load balancing needs
Finally add this to the application.properties of the api-gateway to accomodate the new product-service
## Product service route
spring.cloud.gateway.routes[2].id=product-service
#the lb:// is for load balancing
spring.cloud.gateway.routes[2].uri=lb://product-service
#spring.cloud.gateway.mvc.routes[0].uri=http://product-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/api/product
You would notice that the routes follow an array indexing pattern i.e 0,1,2
With this setup you can navigate to http://localhost:8080/product-service/whatever-endpoint and it would require oauth2 authorization with your keycloak realm secret and you and your application would be fine 😁😁😁
The code for the project is on this github repo. Till next time cheerio ✌️✌️