16 Case Practice Building Microservice Security Architecture Based on Spring Security and Spring Cloud

16 Case Practice Building Microservice Security Architecture Based on Spring Security and Spring Cloud #

Through the previous courses, we have learned that Spring Security can integrate with the OAuth2 protocol to achieve access authorization in a distributed environment. At the same time, Spring Security can seamlessly integrate with the Spring Cloud framework and complete the permission control of each microservice.

Today, we will design a case system to build a complete microservice system from scratch. In addition to demonstrating the process of building a microservice system, we will also focus on the role of the OAuth2 protocol and JWT in it.

Case Scenario: SpringAppointment #

In this course, we will build a relatively simple complete system to demonstrate the design concepts and technical components related to microservice architecture. This case system is called SpringAppointment.

SpringAppointment includes a relatively simple business scenario that can be used to simulate the appointment handling process in the medical process. Generally, the appointment process will involve three independent microservices: the card service, the appointment service, and the doctor service.

We refer to the above three services as business services. Looking at the entire SpringAppointment system, in addition to these three business microservices, there are also a number of non-business infrastructure services, including the registry service (Eureka), the configuration center service (Spring Cloud Config), and the API gateway service (Zuul). The construction process of infrastructure services in Spring Cloud is not the focus of this column, you can refer to the column “Spring Cloud Principles and Practices” on LaGou for more detailed understanding.

Although the services in the case are physically independent, the entire system requires the services to cooperate with each other to form a complete microservice system. That is to say, there is a certain dependency at runtime. We will organize the operation of SpringAppointment based on the system architecture. The basic method is to build independent services according to the service list and manage their dependencies based on the registry, as shown in the following figure:

Image 1

Service runtime dependency diagram based on the registry

Build OAuth2 Authorization Service #

In the above figure, we notice that there is still one more infrastructure microservice in the case system, which is the OAuth2 authorization service, which plays the role of an authorization center here. The specific construction steps of the OAuth2 authorization service have been detailed in “[Authorization System: How to Integrate the OAuth2 Protocol in Microservice Architecture?]” Here, we directly create a subclass of WebSecurityConfigurerAdapter named WebSecurityConfigurer

and a subclass of AuthorizationServerConfigurerAdapter named JWTOAuth2Config. The implementation code is as follows:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.inMemoryAuthentication().withUser("user").password("{noop}password1").roles("USER").and()
                .withUser("admin").password("{noop}password2").roles("USER", "ADMIN");
    }

}

@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("appointment_client").secret("{noop}appointment_secret")
                .authorizedGrantTypes("refresh_token", "password", "client_credentials")
                .scopes("webclient", "mobileclient");
    }

}

Initializing Business Services #

In the SpringAppointment sample system, we need to build three business microservices: card-service, appointment-service, and doctor-service. They are all independent Spring Boot applications. When building the business services, we need to first integrate them with infrastructure-class services. Since the API Gateway serves as a service router, it is transparent to the various business services. However, other registry centers, configuration centers, and authorization centers require each business service to complete integration with them.

Integrating with the Registry Center #

For the registry center Eureka, card-service, appointment-service, and doctor-service are all its clients, so they need the dependency spring-cloud-starter-netflix-eureka-client, as shown below.

    
<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

</dependency>

Next, let’s take appointment-service as an example to look at its Bootstrap class, as shown below.

    
@SpringBootApplication

@EnableEurekaClient

public class AppointmentApplication {

    public static void main(String[] args) {
    
        SpringApplication.run(AppointmentApplication.class, args);

    }

}

In this code snippet, a new annotation @EnableEurekaClient is introduced, which indicates that the current service is a Eureka client. This allows the service to be automatically registered with the Eureka server. Of course, we can also directly use the unified @SpringCloudApplication annotation to achieve the integration of @SpringBootApplication and @EnableEurekaClient annotations.

The next step is the most important configuration work. The configuration of appointment-service is shown below.

spring:

  application:

    name: appointmentservice

server:

  port: 8081

eureka:

  client:

    registerWithEureka: true

    fetchRegistry: true

    serviceUrl:

      defaultZone: http://localhost:8761/eureka/

Clearly, this configuration contains two parts. In the first part, the name and runtime port of the service are specified. In the above example, the name of appointment-service is specified as “appointmentservice” using spring.application.name=appointmentservice. This means that the name of appointment-service in the registry center is “appointmentservice”. In the subsequent examples, we will use this name to obtain various registration information of appointment-service in Eureka.

Integrating with the Configuration Center #

To obtain configuration information from the configuration server, we first need to initialize the client, that is, integrate the various microservices with the Spring Cloud Config server. The first step in initializing the client is to introduce the Spring Cloud Config client component spring-cloud-config-client as shown below.

    
<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-config-client</artifactId>

</dependency>

Then we need to configure the access address of the server in the application.yml configuration file, as shown below.

spring: 

  cloud:

    config:

       enabled: true

       uri: http://localhost:8888

In the above configuration, we specify the address of the configuration server, which is the uri mentioned above: http://localhost:8888.

Once we introduce the Spring Cloud Config client component, it means that the functionality of accessing the HTTP endpoints of the configuration server is automatically integrated into various microservices. In other words, the process of accessing the configuration server is transparent to the microservices. That is, the microservices do not need to consider how to obtain configuration information from remote servers, but only need to consider how to use this configuration information in the Spring Boot application. For common relational data access configurations, Spring has already built-in the integration process for us. What we need to do is to introduce the relevant dependency components.

Let’s take appointment-service as an example to demonstrate the database access function. The example uses JPA and MySQL, so we need to introduce the relevant dependencies in the service, as shown below.

<dependency>

       <groupId>org.springframework.boot</groupId>

       <artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>



<dependency>
<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

Now, we can use JPA’s data access capabilities to access the MySQL database.

Integrating Authorization Center #

In the business service, the implementation method of integrating the authorization center has been detailed in “14. Resource Protection: How to use the OAuth2 protocol to authorize access to microservices?”. Here is a brief recap. First, we need to add the @EnableResourceServer annotation to the Spring Boot startup class:

@SpringCloudApplication
@EnableResourceServer
public class AppointmentApplication {

    public static void main(String[] args) {
        SpringApplication.run(AppointmentApplication.class, args);
    }

}

Next, we need to specify the address of the authorization center service in the configuration file:

security:
  oauth2:
    resource:
      userInfoUri: http://localhost:8080/userinfo

Finally, what needs to be done is to embed access control in each business service. We can use any of the three strategies: user-level access control, user + role-level access control, and user + role + operation-level access control to achieve this goal.

Integrating and Extending JWT #

Let’s go back to the SpringAppointment case system again. Taking the business scenario of placing an order by a user as an example, it involves the interaction between the appointment-service, doctor-service, and card-service. The interaction between these three services is as shown in the following diagram: Image

Interaction diagram of the three business microservices in the SpringAppointment case system

Based on this interaction diagram, we can actually outline the code structure for this scenario as follows:

public Appointment generateAppointment(String doctorName, String cardCode) {
 
    Appointment appointment = new Appointment();
 
    // Get remote Card information
    CardMapper card = getCard(cardCode);
    ...
 
    // Get remote Doctor information
    DoctorMapper doctor = getDoctor(doctorName);
    ...
 
    appointmentRepository.save(appointment);
 
    return appointment;
}

In this code, the appointment-service retrieves the Card object from the card-service and the Doctor object from the doctor-service. Both of these steps involve accessing remote web services. Therefore, we first need to create corresponding HTTP endpoints in the card-service and doctor-service services. This process is not the focus of this course. If you are interested, you can refer to the sample code to learn more: https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/SpringAppointment.

Integrating JWT #

In “15 | Token Extension: How to use JWT to implement custom tokens?”, we introduced JWT and completed its integration with the OAuth2 protocol, thereby achieving customization of the token. JWT also needs to be transmitted throughout the entire service invocation process. The client holding the JWT accesses the order placement operation provided by the appointment-service using HTTP endpoints. The service will validate the validity of the JWT passed in. Then, the appointment-service will access the card-service and doctor-service through the gateway. Similarly, these two services will also verify the JWT passed in and return the corresponding results.

Now, let’s create a CardRestTemplateClient class in the appointment-service. It can be seen that it uses the RestTemplate object created in “15 | Token Extension: How to use JWT to implement custom tokens?” to make remote calls. The code is as follows:

@Service
public class CardRestTemplateClient {

    @Autowired
    RestTemplate restTemplate;

    public CardMapper getCardByCardCode(String cardCode) {
        ResponseEntity<CardMapper> result = restTemplate.exchange("http://cardservice/cards/{cardCode}", HttpMethod.GET, null,
                CardMapper.class, cardCode);

        return result.getBody();
    }
}

}

We know that in this RestTemplate, the request is intercepted based on the AuthorizationHeaderInterceptor, which completes the correct propagation of JWT in various services.

Finally, we use Postman to verify the correctness of the above process. By accessing the endpoint configured in Zuul and passing the Token information corresponding to the user with the role “ADMIN”, we can see that the order record has been successfully created. You can try generating different Tokens to execute this process and verify the authorization effect.

Extending JWT #

In the end, let’s discuss how to extend JWT. JWT has good extensibility, and developers can add various additional information they want to JWT Tokens as needed.

For the scenario of extending JWT, Spring Security provides a TokenEnhancer interface specifically for enhancing tokens. The interface is defined as follows:

public interface TokenEnhancer {

    OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);

}

We can see that it handles an OAuth2AccessToken interface, and this interface has a default implementation class called DefaultOAuth2AccessToken. We can add additional information to the OAuth2AccessToken in the form of key-value pairs through the setAdditionalInformation method of this implementation class. The sample code is as follows:

public class JWTTokenEnhancer implements TokenEnhancer {



    @Override

    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

        Map<String, Object> systemInfo = new HashMap<>();

        systemInfo.put("system", "Appointment System");



        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(systemInfo);

        return accessToken;

    }

}

Here, we added a “system” attribute in a hard-coded way. You can adjust it according to your needs.

To make the above JWTTokenEnhancer class take effect, we need to reconfigure the configure method in the JWTOAuth2Config class and embed the JWTTokenEnhancer into the TokenEnhancerChain, as shown below:

@Override

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();

        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter)); 



        endpoints.tokenStore(tokenStore)

                .accessTokenConverter(jwtAccessTokenConverter)

                .tokenEnhancer(tokenEnhancerChain)

                .authenticationManager(authenticationManager)

                .userDetailsService(userDetailsService);

}

Please note that we embed multiple TokenEnhancers, including JWTTokenEnhancer, into TokenEnhancerChain by creating a list of TokenEnhancers.

Now, we have extended the JWT Token. So, how to retrieve the extended attributes from this JWT Token? The method is also simple and fixed, as shown below:

// Get JWTToken

RequestContext ctx = RequestContext.getCurrentContext();

String authorizationHeader = ctx.getRequest().getHeader(AUTHORIZATION_HEADER);

String jwtToken = authorizationHeader.replace("Bearer ","");

// Parse JWTToken

String[] split_string = jwtToken.split("\\.");

String base64EncodedBody = split_string[1];

Base64 base64Url = new Base64(true);

String body = new String(base64Url.decode(base64EncodedBody));

JSONObject jsonObj = new JSONObject(body);

// Get customized attribute value

String systemName = jsonObj.getString("system");

We can embed this code into any scenario where we need to use the custom “system” attribute.

Summary and Preview #

Case analysis is the best way to master the application of a framework, and it is the same for the OAuth2 protocol. In this lesson, we combined Spring Security with Spring Cloud to build a microservice case system called SpringAppointment. Then, based on the business scenarios in the SpringAppointment case, we divided the various microservices and focused on the construction process of each business service. On the one hand, we demonstrated the integration process of business services and infrastructure services, and on the other hand, we also demonstrated the implementation process of integrating and extending JWT.

Finally, let me leave you with a question: How to implement customized extension of JWT in a business system? Feel free to share your thoughts and insights in the comments section.