Build J2EE micro services architecture by using Spring Boot, Spring Cloud, Spring Security OAuth2, KeyCloak

I posted an article in regards to single page application(UI), but in this post I'm going to introduce how to build micro service architecture for J2EE application with Spring framework and open source SSO framework Keycloak. This post will cover the following aspects:

  • Keycloak setup
  • Eureka service registration and discovery
  • Spring Cloud API gateway
  • Spring Security (OAuth2 login) and the integration with Keycloak
  • Micro services


The code is available in my Github and please check the docker-compose.yml at first so that you can read the rest of the post easier. One thing I need to mention here is you need to replace the ip address of keycloak server url with your own before running the docker containers.

YAML
 


1
version: '3.4'
2
services:
3
  api-gateway:
4
    build:
5
      context: ./api-gateway
6
    ports:
7
      - "8080:8080"
8
    restart: on-failure
9
    environment:
10
      #overriding spring application.properties
11
      - eureka.client.serviceUrl.defaultZone=http://eureka-server:9091/eureka/
12
      - keycloak-client.server-url=http://10.0.0.17:18080/auth # use host name or ip of the host machine
13
    depends_on:
14
      - eureka-server
15
  eureka-server:
16
    build:
17
      context: ./eureka-server
18
    ports:
19
      - "9091:9091"
20
    restart: on-failure
21
  microservice-consumer:
22
    build:
23
      context: ./microservice-consumer
24
    ports:
25
      - "9080:9080"
26
    restart: on-failure
27
    environment:
28
      #overriding spring application.properties
29
      - eureka.client.serviceUrl.defaultZone=http://eureka-server:9091/eureka/
30
      - keycloak-client.server-url=http://10.0.0.17:18080/auth # use host name or ip of the host machine
31
    depends_on:
32
      - eureka-server
33
  microservice-producer:
34
    build:
35
      context: ./microservice-producer
36
    ports:
37
      - "9081:9081"
38
    restart: on-failure
39
    environment:
40
      #overriding spring application.properties
41
      - eureka.client.serviceUrl.defaultZone=http://eureka-server:9091/eureka/
42
      - keycloak-client.server-url=http://10.0.0.17:18080/auth # use host name or ip of the host machine
43
    depends_on:
44
      - eureka-server
45
  keycloak:
46
    image: jboss/keycloak:11.0.0
47
      volumes:
48
      - ./keycloak-server/realm-export.json:/tmp/keycloak/config/realm-export.json
49
    environment:
50
        KEYCLOAK_USER: admin
51
        KEYCLOAK_PASSWORD: admin
52
        KEYCLOAK_IMPORT: /tmp/keycloak/config/realm-export.json
53
        DB_VENDOR: POSTGRES
54
        DB_ADDR: postgres
55
        DB_DATABASE: keycloak
56
        DB_USER: keycloak
57
        DB_SCHEMA: public
58
        DB_PASSWORD: password
59
    ports:
60
      - "18080:18080"
61
    command:
62
      - "-b"
63
      - "0.0.0.0"
64
      - "-Djboss.socket.binding.port-offset=10000"
65
    restart: on-failure
66
    depends_on:
67
      - postgres
68
  postgres:
69
      image: postgres
70
      volumes:
71
        - postgres_data:/var/lib/postgresql/data
72
      environment:
73
        POSTGRES_DB: keycloak
74
        POSTGRES_USER: keycloak
75
        POSTGRES_PASSWORD: password
76
volumes:
77
    postgres_data:
78
      name: keycloak_postgres_data
79
      driver: local


Then, you can run the following cmd to start up docker containers.

Shell
 

1
docker-compose up --build



1.Keycloak

Keycloak is an open source identity and access management solution. It's convenient to integrate with Spring Security OAuth2 framework. Here I'm showing step by step how to setup a keycloak server. 
First of all, based on the docker compose profile above, please use the following cmd to build and start a keycloak server. I have exported a json file(realm-export.json) that can be consumed by keycloak docker container, so that you don't need to configure step by step. Anyway, I will show that steps one by one.
Shell
 

1
docker-compose up --build keycloak




1.1 Create a realm

Login http://localhost:18080/ with admin/admin, then create a realm as below, leaving all the fields as default.

1.2 Create clients under the above realm.


As shown above, we created three clients which represent api-gateway, micro service consumer, micro service producer. There are two things necessary to mention.


First, fill mandatory field(Valid Redirect URIs) as * so that we can redirect back to our URI configured in our application.

Then, choose client id and secrete as Client Authenticator, which is suitable for the integration with  Spring Security OAuth2 client.

1.3 Create a user

As shown in the screenshot below, we need fill some necessary fields like first name, last name and email etc.

2. Eureka Sever - Service registration

It's quite straight forward to build a service registration server based on Spring Cloud Eureka, and we just need annotation @EnableEurekaServer to enable that.

3. API Gateway

Spring framework 5 releases Spring Cloud gateway to replace previous gateway - Spring Cloud Zuul. Spring Cloud gateway is a new generation non blocking gateway built on top of Project Reactor, Spring WebFlux, and Spring Boot 2.0, while Zuul is a blocking gateway.  So, in this post, I use Spring Cloud Gateway as an api gateway for the micro services.

First of all, I need to show the application.yml configuration profile.

YAML
 


1
logging:
2
  level:
3
    root: WARN
4
    org.springframework.web: INFO
5
    org.springframework.security: DEBUG
6
    org.springframework.security.oauth2: DEBUG
7

8
server:
9
  port: 8080
10
keycloak-client:
11
  server-url: http://localhost:18080/auth
12
  realm:  spring-micro-main
13
spring:
14
  application:
15
    name: api-gateway
16
  security:
17
    oauth2:
18
      client:
19
        registration:
20
          keycloak:
21
            provider: keycloak
22
            client-id: spring-micro-gateway
23
            client-secret: 756b0558-018b-4809-b478-bd5b4995d325
24
            authorization-grant-type: authorization_code
25
            redirect-uri: http://localhost:8080/login/oauth2/code/keycloak
26
            scope: openid
27
        provider:
28
          keycloak:
29
            authorization-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}/protocol/openid-connect/auth
30
            token-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}/protocol/openid-connect/token
31
            user-info-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}/protocol/openid-connect/userinfo
32
            jwk-set-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}/protocol/openid-connect/certs
33
            user-name-attribute: name
34
            user-info-authentication-method: header
35
      resourceserver:
36
        jwt:
37
          jwk-set-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}/protocol/openid-connect/certs
38
  cloud:
39
    gateway:
40
      routes:
41
        - id: microservice-consumer
42
          uri: lb://microservice-consumer
43
          predicates:
44
            - Path=/api/consume/**
45
          filters:
46
            - TokenRelay=
47
            - RemoveRequestHeader=Cookie
48
        - id: microservice-producer
49
          uri: lb://microservice-producer
50
          predicates:
51
            - Path=/api/produce/**
52
          filters:
53
            - TokenRelay=
54
            - RemoveRequestHeader=Cookie
55

56
eureka:
57
  client:
58
    serviceUrl:
59
      defaultZone: http://localhost:9091/eureka/
60




3.1 Route definition in Spring Cloud Gateway

We defined two routes for two micro services: consumer and producer. The lb://microservice-consumer means it points to microservice-consumer based on service registration server - Eureka. We use TokenRelay so that the access_token from the request can be passed through to micro service resource server which is secured by Spring Security OAuth2 along with Keycloak as well.


3.2 Eureka client configuration

The API gateway also acts as a Eureka client so that it can work with micro service replicas. We just need an annotation @EnableEurekaClient and configure eureka server url in the properties.

YAML
 

1
eureka:
2
  client:
3
    serviceUrl:
4
      defaultZone: http://localhost:9091/eureka/



3.3 Spring Security OAuth2 and the integration with Keycloak server.

3.3.1 Spring Security OAuth2 

One of the key features of Spring Security 5 is the native support for OAuth2 and OIDC, instead of the legacy client support in the old Spring Security OAuth sub project, integrating with IAM(Identity and Access Management) providers gets super easy. We need to add the following dependencies:

XML
 

1
<dependency>
2
  <groupId>org.springframework.boot</groupId>
3
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
4
</dependency>
5
<dependency>
6
  <groupId>org.springframework.cloud</groupId>
7
  <artifactId>spring-cloud-starter-security</artifactId>
8
</dependency>



3.3.2 OAuth2 client registration

The properties for OAuth 2 clients are prefixed with spring.security.oauth2.client.registration andspring.security.oauth2.client.provider. For Keycloak specifically, we have to configure the details of the OAuth2 provider, and provides the details of client registration as below.

YAML
 




3.3.3 Spring Security OAuth2 resource server

Keycloak uses a public/private key pair to issue and verify the JWT(JSON Web Token). In particular, it uses private key to sign the access token, and the OAuth2 client uses public key to verify the token. 

We need to protect the API gateway as an OAuth2 resource server so that when a request coming in with a bearer token under the Authorization header, it can also verify the token rather than redirect to SSO server for authentication, then the request can eventually go to downstream micro services. For example, we can use a http client to issue a request as below.

HTTP
 

1
GET localhost:8080/api/produce/
2
Authorization: Bearer <your access token>



3.3.4 Enable OAuth2 login and resource server

In order to enable Spring Security OAuth2 login as well as resource server, we need leverage DSL methods.

Spring Security’s OAuth 2.0 Login support is enabled via the Spring Security oauth2Login() DSL method.

Spring Security’s Resource Server support is enabled via the Spring Security oauth2ResourceServer DSL method.

Illustrated as below:

Java
 


1
@EnableWebFluxSecurity
2
@EnableReactiveMethodSecurity
3
public class SpringSecurityConfig {
4

5
    @Bean
6
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
7
        // @formatter:off
8
        http
9
                .authorizeExchange()
10
                .anyExchange().authenticated()
11
                .and()
12
                .oauth2Login()
13
                .and()
14
                .oauth2ResourceServer()
15
                .jwt();
16
        return http.build();
17
        // @formatter:on
18
    }
19
}



4. Micro services

Micro services should be protected by Keycloak and enabled as OAuth2 resource server as well. So the configuration is similar to the API gateway. One key difference is that Spring Cloud gateway is built on top of Spring WebFlux while our micro services are built on top of Spring MVC. So the security configuration has a slight difference, illustrated as below.

Java
 


1
@Configuration
2
public class OAuth2ResourceServerConfig extends WebSecurityConfigurerAdapter {
3

4
    @Override
5
    protected void configure(HttpSecurity http) throws Exception {
6
        http.authorizeRequests()
7
                .anyRequest().authenticated()
8
                .and()
9
                .oauth2Login()
10
                .and()
11
                .oauth2ResourceServer()
12
                .jwt();
13
    }
14
}



That's it and you are good to go. Please check the source code from my Github.

Comments

  1. While importing the realm-export.json in keycloak I get 'an 'Unexpected Error" message. What's the error? Can you look?

    ReplyDelete
  2. Hi Kunkka, Can you let me know whether this example redirects the clients to Keycloak login page when the client hits one of the microservice via the CloudGateway? I'm looking for an example of app to app communication for microservice. So the gateway and services should verify the bearer token by calling any of the keycloak APIs.

    ReplyDelete
    Replies
    1. For sure once API Gateway as well as micro services are protected by Keycloak, token verification will fail and will redirect to Keycloak server.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. One more question. Here I have registered my services and gateway as clients in Keycloak. I generate the token using the gateway client. With the same token I can access the services directly without the need to go through gateway. Is there anyway I can restrict the direct access to the microservices and allow traffic only via gateway?

      Delete
    4. You can only export 80 and 443 port which are used by gateway so that outside traffic cannot reach your internal services.

      Delete
  3. 2021-05-09 17:54:27.422 ERROR 1 --- [or-http-epoll-4] a.w.r.e.AbstractErrorWebExceptionHandler : [4d469354-1] 500 Server Error for HTTP GET "/api/produce/"

    java.lang.IllegalStateException: Could not obtain the keys
    at org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder$JwkSetUriReactiveJwtDecoderBuilder.lambda$null$1(NimbusReactiveJwtDecoder.java:345) ~[spring-security-oauth2-jose-5.3.4.RELEASE.jar!/:5.3.4.RELEASE]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
    Error has been observed at the following site(s):
    |_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$OAuth2ResourceServerSpec$BearerTokenAuthenticationWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.csrf.CsrfWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    |_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]

    ReplyDelete
    Replies
    1. This exception means your JWT public key is not accessible by the OAuth2 client. Please login keycloak web portal to check the public key. Also please check the following uri is working after you start key cloak server.
      jwk-set-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}/protocol/openid-connect/certs

      Delete
    2. http://localhost:18080/local/realms/spring-micro-main/protocol/openid-connect/certs
      404 Not Found

      I get a successful response after changing 10.0.0.17 to my host ip

      Delete
  4. I am getting duplicate bean error (TokenRelay defined multiple time one in oauth2-client and another in cloud security) using spring boot 2.5.2 and spring cloud 2020.0.3

    ReplyDelete
  5. This comment has been removed by a blog administrator.

    ReplyDelete
  6. This comment has been removed by a blog administrator.

    ReplyDelete

Post a Comment

Popular posts from this blog

NGINX and HTTPs with Let’s Encrypt, Certbot, and Cron dockerization in production

Vault Cubbyhole authentication and its integration with Spring framework