A+Microservice+Architecture+With+Spring+Boot+and+Spring+Cloud
A+Microservice+Architecture+With+Spring+Boot+and+Spring+Cloud
V 4.0.0
1. Spring Cloud – Bootstrapping
1. Overview 9
2. Config Server 10
2.1. Setup 10
2.3. Properties 12
2.5. Run 13
3. Discovery 15
3.1. Setup 15
3.3. Properties 16
3.5. Run 18
4. Gateway 19
4.1. Setup 19
4.3. Properties 20
4.4. Run 21
5. Book Service 22
5.1. Setup 22
5.3. Properties 24
5.4. Run 25
6. Rating Service 26
6.1. Setup 26
6.3. Properties 28
6.4. Run 29
7. Conclusion 30
2. Routing Handler 33
3. Dynamic Routing 34
4. Routing Factories 35
5. WebFilter Factories 36
7. Monitoring 38
8. Implementation 39
8.1. Dependencies 39
9. Conclusion 41
2. Maven Setup 44
6.2. Properties 55
7.2. Properties 57
9. Conclusion 62
2.1. Setup 65
2.3. Configuration 66
2.4. Run 67
2.5.1. Setup 67
2.5.2. Configuration 68
3.3. Run 69
4. Conclusion 70
2. Gateway Changes 73
4. Angular 80
4.1. Template 80
4.2. Typescript 82
4.3. HttpService 84
5. Conclusion 89
1. Spring Cloud – Bootstrapping
8
1. Overview
9
2. Config Server
To learn more details and see a more complex example, take a look at our
Spring Cloud Configuration chapter.
2.1. Setup
We’ll set the artifact to “config.” In the dependencies section, we’ll search for
“config server” and add that module. Then we’ll press the generate button,
which allows us to download a zip file with a preconfigured project already
inside and ready to go.
1. <parent>
2. <groupId>org.springframework.boot</groupId>
3. <artifactId>spring-boot-starter-parent</artifactId>
4. <version>2.7.8</version>
5. <relativePath/>
6. </parent>
10
7. <dependencies>
8. <dependency>
<groupId>org.springframework.boot</groupId>
9.
<artifactId>spring-boot-starter-test</artifactId>
10.
<scope>test</scope>
11. </dependency>
12. </dependencies>
13. <dependencyManagement>
14. <dependencies>
15. <dependency>
16. <groupId>org.springframework.cloud</groupId>
17. <artifactId>spring-cloud-dependencies</artifactId>
18. <version>2021.0.7</version>
19. <type>pom</type>
20. <scope>import</scope>
21. </dependency>
22. </dependencies>
23. </dependencyManagement>
24. <build>
25. <plugins>
26. <plugin>
27. <groupId>org.springframework.boot</groupId>
28. <artifactId>spring-boot-maven-plugin</artifactId>
29. </plugin>
30. </plugins>
31. </build>
1. <dependency>
2. <groupId>org.springframework.cloud</groupId>
3. <artifactId>spring-cloud-config-server</artifactId>
4. </dependency>
11
For reference, we can find the latest version on Maven Central
(spring-cloud-dependencies, test, config-server).
1. @SpringBootApplication
2. @EnableConfigServer
3. public class ConfigApplication {...}
2.3. Properties
spring.cloud.config.server.git.uri=file://${user.home}/application-config
The most significant setting for the config server is the git.uri parameter.
This is currently set to a relative file path that generally resolves to c:\
Users\{username}\ on Windows, or /Users/{username}/ on *nix. This
property points to a Git repository where the property files for all the other
applications are stored. It can be set to an absolute file path if necessary.
Tip: On a windows machine preface the value with “file:///”, on *nix then use
“file://”.
12
2.4. Git Repository
2.5. Run
Let’s run config server and make sure it’s working. From the command line,
we’ll type mvn spring-boot:run. This will start the server.
It’s a bootstrap process, and each one of these apps is going to have a file
called bootstrap.properties. It will contain properties just like application.
properties, but with a twist.
13
Finally, since Config Server is managing our application properties, one
might wonder why even have an application.properties at all. The answer is
that these still come in handy as default values that perhaps Config Server
doesn’t have.
14
3. Discovery
Now that we have configuration taken care of, we need a way for all of our
servers to be able to find each other. We’ll solve this problem by setting up
the Eureka discovery server. Since our applications could be running on any
ip/port combination, we need a central address registry that can serve as
an application address lookup.
3.1. Setup
Alternatively, we can create a Spring Boot project, copy the contents of the
POM from config server, and swap in these dependencies:
1. <dependency>
2. <groupId>org.springframework.cloud</groupId>
3. <artifactId>spring-cloud-starter-config</artifactId>
4. </dependency>
5. <dependency>
6. <groupId>org.springframework.cloud</groupId>
7. <artifactId>spring-cloud-starter-bootstrap</artifactId>
8. </dependency>
15
9. <dependency>
10. <groupId>org.springframework.cloud</groupId>
11. <artifactId>spring-cloud-starter-netflix-eureka-server</
12. artifactId>
13. </dependency>
1. @SpringBootApplication
2. @EnableEurekaServer
3. public class DiscoveryApplication {...}
3.3. Properties
Now we’ll add two properties files. First, we’ll add bootstrap.properties into
src/main/resources:
spring.cloud.config.name=discovery
spring.cloud.config.uri=http://localhost:8081
These properties will let the discovery server query the config server at
startup.
16
spring.application.name=discovery
server.port=8082
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://localhost:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
In addition, we’re telling this server that it’s operating in the default zone,
which matches the config client’s region setting. We’re also telling the
server not to register with another discovery instance.
Let’s commit the file to the Git repository. Otherwise, the file won’t be
detected.
1. <dependency>
2. <groupId>org.springframework.cloud</groupId>
3. <artifactId>spring-cloud-starter-netflix-eureka-client</
4. artifactId>
5. </dependency>
6. <dependency>
7. <groupId>org.springframework.cloud</groupId>
8. <artifactId>spring-cloud-starter-bootstrap</artifactId>
9. </dependency>
17
For reference, we can find the bundle on Maven Central (eureka-client).
3.5. Run
Now we’ll start the discovery server using the same command, mvn spring-
boot:run. The output from the command line should include:
Fetching config from server at: http://localhost:8081
...
Tomcat started on port(s): 8082 (http)
Let’s stop and rerun the config service. If everything is correct, the output
should look like:
DiscoveryClient_CONFIG/10.1.10.235:config:8081: registering service...
Tomcat started on port(s): 8081 (http)
DiscoveryClient_CONFIG/10.1.10.235:config:8081 - registration status: 204
18
4. Gateway
4.1. Setup
1. <dependency>
2. <groupId>org.springframework.cloud</groupId>
3. <artifactId>spring-cloud-starter-config</artifactId>
4. </dependency>
5. <dependency>
6. <groupId>org.springframework.cloud</groupId>
7. <artifactId>spring-cloud-starter-netflix-eureka-client</
8. artifactId>
9. </dependency>
19
10. <dependency>
11. <groupId>org.springframework.cloud</groupId>
12. <artifactId>spring-cloud-starter-gateway</artifactId>
13. </dependency>
14. <dependency>
15. <groupId>org.springframework.cloud</groupId>
16. <artifactId>spring-cloud-starter-bootstrap</artifactId>
17. </dependency>
1. @SpringBootApplication
2. @EnableDiscoveryClient
3. @EnableFeignClients
4. public class GatewayApplication {...}
4.3. Properties
bootstrap.properties in src/main/resources:
spring.cloud.config.name=gateway
spring.cloud.config.discovery.service-id=config
spring.cloud.config.discovery.enabled=true
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
eureka.client.serviceUrl.defaultZone=http://localhost:8082/eureka/
20
gateway.properties in our Git repository
spring.application.name=gateway
server.port=8080
eureka.client.region=default
eureka.client.registryFetchIntervalSeconds=5
4.4. Run
We’ll run the config and discovery applications, and wait until the config
application has registered with the discovery server. If they’re already
running, we don’t have to restart them. Once that’s complete, we’ll run the
gateway server. The gateway server should start on port 8080 and register
itself with the discovery server. The output from the console should contain:
Fetching config from server at: http://10.1.10.235:8081/
...
DiscoveryClient_GATEWAY/10.1.10.235:gateway:8080: registering service...
DiscoveryClient_GATEWAY/10.1.10.235:gateway:8080 - registration status:
204
Tomcat started on port(s): 8080 (http)
One mistake that’s easy to make is to start the server before the config
server has registered with Eureka. In that case, we’d see a log with this
output:
Fetching config from server at: http://localhost:8888
This is the default URL and port for a config server, and indicates our
discovery service didn’t have an address when the configuration request
was made. We can just wait a few seconds and try again; once the config
server has registered with Eureka, the problem will resolve.
21
5. Book Service
5.1. Setup
1. <dependency>
2. <groupId>org.springframework.cloud</groupId>
3. <artifactId>spring-cloud-starter-config</artifactId>
4. </dependency>
5. <dependency>
6. <groupId>org.springframework.cloud</groupId>
7. <artifactId>spring-cloud-starter-bootstrap</artifactId>
8. </dependency>
9. <dependency>
10. <groupId>org.springframework.cloud</groupId>
11. <artifactId>spring-cloud-starter-netflix-eureka-client</
12. artifactId>
13. </dependency>
14. <dependency>
15. <groupId>org.springframework.boot</groupId>
16. <artifactId>spring-boot-starter-web</artifactId>
17. </dependency>
22
5.2. Spring Config
1. @SpringBootApplication
2. @EnableDiscoveryClient
3. @EnableFeignClients
4. public class GatewayApplication {...}
23
We also added a REST controller and a field set by our properties file to
return a value we’ll set during configuration.
5.3. Properties
bootstrap.properties in src/main/resources:
spring.cloud.config.name=book-service
spring.cloud.config.discovery.service-id=config
spring.cloud.config.discovery.enabled=true
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
eureka.client.serviceUrl.defaultZone=http://localhost:8082/eureka/
eureka.client.region=default
eureka.client.registryFetchIntervalSeconds=5
eureka.client.serviceUrl.defaultZone=http://localhost:8082/eureka/
24
5.4. Run
Once all the other applications have started, we can start the book service.
The console output should look like:
DiscoveryClient_BOOK-SERVICE/10.1.10.235:book-service:8083: registering
service...
DiscoveryClient_BOOK-SERVICE/10.1.10.235:book-service:8083 -
registration status: 204
Tomcat started on port(s): 8083 (http)
Once it’s up, we can use our browser to access the endpoint we just
created. When we navigate to http://localhost:8080/book-service/books,
we get back a JSON object with two books we added in our controller.
Notice that we’re not accessing the book service directly on port 8083, and
instead we’re going through the gateway server.
25
6. Rating Service
Like our book service, our rating service will be a domain driven service that
will handle operations related to ratings.
6.1. Setup
1. <dependency>
2. <groupId>org.springframework.cloud</groupId>
3. <artifactId>spring-cloud-starter-config</artifactId>
4. </dependency>
5. <dependency>
6. <groupId>org.springframework.cloud</groupId>
7. <artifactId>spring-cloud-starter-bootstrap</artifactId>
8. </dependency>
9. <dependency>
10. <groupId>org.springframework.cloud</groupId>
11. <artifactId>spring-cloud-starter-netflix-eureka-client</
12. artifactId>
13. </dependency>
14. <dependency>
15. <groupId>org.springframework.boot</groupId>
16. <artifactId>spring-boot-starter-web</artifactId>
17. </dependency>
26
6.2. Spring Config
1. @SpringBootApplication
2. @EnableDiscoveryClient
3. @RestController
4. @RequestMapping(“/ratings”)
27
We also added a REST controller and a field set by our properties file to
return a value we’ll set during configuration.
6.3. Properties
bootstrap.properties in src/main/resources:
spring.cloud.config.name=rating-service
spring.cloud.config.discovery.service-id=config
spring.cloud.config.discovery.enabled=true
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
eureka.client.serviceUrl.defaultZone=http://localhost:8082/eureka/
eureka.client.region=default
eureka.client.registryFetchIntervalSeconds=5
eureka.client.serviceUrl.defaultZone=http://localhost:8082/eureka/
28
6.4. Run
Once all the other applications have started, we can start the rating service.
The console output should look like:
DiscoveryClient_RATING-SERVICE/10.1.10.235:rating-service:8083:
registering service...
DiscoveryClient_RATING-SERVICE/10.1.10.235:rating-service:8083 -
registration status: 204
Tomcat started on port(s): 8084 (http)
Once it’s up, we can use our browser to access the endpoint we just
created. Let’s navigate to http://localhost:8080/rating-service/ratings/all
and we get back JSON containing all of our ratings. Notice that we’re not
accessing the rating service directly on port 8084, but we’re going through
the gateway server.
29
7. Conclusion
Now we’re able to connect the various pieces of Spring Cloud into a
functioning microservice application. This forms a base that we can use to
begin building more complex applications.
30
2. Exploring the New Spring Cloud Gateway
31
1. Overview
In this chapter, we’ll explore the main features of the Spring Cloud Gateway
project, a new API based on Spring 6, Spring Boot 3, and Project Reactor.
32
2. Routing Handler
Let’s start with a quick example of how the Gateway Handler resolves route
configurations by using RouteLocator:
1. @Bean
2. public RouteLocator customRouteLocator(RouteLocatorBuilder
3. builder) {
4. return builder.routes()
5. .route(“r1”, r -> r.host(“**.baeldung.com”)
6. .and()
7. .path(“/baeldung”)
8. .uri(“http://baeldung.com”))
9. .route(r -> r.host(“**.baeldung.com”)
10. .and()
11. .path(“/myOtherRouting”)
12. .filters(f -> f.prefixPath(“/myPrefix”))
13. .uri(“http://othersite.com”)
14. .id(“myOtherID”))
15. .build();
16. }
Notice how we made use of the main building blocks of this API:
33
3. Dynamic Routing
Just like Zuul, Spring Cloud Gateway provides a means for routing requests
to different services.
34
4. Routing Factories
It also includes many built-in Route Predicate Factories. All these predicates
match different attributes of the HTTP request. Multiple Route Predicate
Factories can be combined via the logical “and.”
35
5. WebFilter Factories
36
6. Spring Cloud DiscoveryClient Support
Spring Cloud Gateway can be easily integrated with Service Discovery and
Registry libraries, such as Eureka Server and Consul:
1. @Configuration
2. @EnableDiscoveryClient
3. public class GatewayDiscoveryConfiguration {
4.
5. @Bean
6. public DiscoveryClientRouteDefinitionLocator
7. discoveryClientRouteLocator(DiscoveryClient discoveryClient)
8. {
9.
10. return new
11. DiscoveryClientRouteDefinitionLocator(discoveryClient);
12. }
13. }
If the URL has a lb scheme (e.g., lb://baeldung-service), it’ll use the Spring
Cloud LoadBalancerClient to resolve the name (i.e., baeldung-service) to an
actual host and port.
37
7. Monitoring
Spring Cloud Gateway makes use of the Actuator API, a well-known Spring
Boot library that provides several out-of-the-box services for monitoring the
application.
Once the Actuator API is installed and configured, the gateway monitoring
features can be visualized by accessing /gateway/ endpoint.
38
8. Implementation
We’ll now create a simple example showcasing how to use Spring Cloud
Gateway as a proxy server using the path predicate.
8.1. Dependencies
1. <dependency>
2. <groupId>org.springframework.boot</groupId>
3. <artifactId>spring-cloud-starter-gateway</artifactId>
4. <version>4.1.1</version>
5. </dependency>
39
Here’s the Gateway application code:
1. @SpringBootApplication
2. public class GatewayApplication {
3. public static void main(String[] args) {
4. SpringApplication.run(GatewayApplication.class, args);
5. }
6. }
1. {
2. “id”:”baeldung_route”,
3. “predicates”:[{
4. “name”:”Path”,
5. “args”:{“_genkey_0”:”/baeldung”}
6. }],
7. “filters”:[],
8. “uri”:”http://baeldung.com”,
9. “order”:0
10. }
40
9. Conclusion
In this chapter, we explored some of the features and components that are
part of Spring Cloud Gateway. This new API provides out-of-the-box tools
for gateway and proxy support.
41
3. Spring Cloud – Securing Services
42
1. Overview
We’ll use Spring Security to share sessions using Spring Session and Redis.
This method is simple to set up and easy to extend to many business
scenarios. If you’re unfamiliar with Spring Session, check out this chapter.
Sharing sessions gives us the ability to log users in our gateway service,
and propagate that authentication to any other service of our system.
43
2. Maven Setup
1. <dependency>
2. <groupId>org.springframework.boot</groupId>
3. <artifactId>spring-boot-starter-security</artifactId>
4. </dependency>
1. <dependency>
2. <groupId>org.springframework.session</groupId>
3. <artifactId>spring-session-data-redis</artifactId>
4. </dependency>
5. <dependency>
6. <groupId>org.springframework.boot</groupId>
7. <artifactId>spring-boot-starter-data-redis</artifactId>
8. </dependency>
Only four of our applications will tie into Spring Session: discovery, gateway,
book-service, and rating-service.
Next, we’ll add a session configuration class in all three services in the same
directory as the main application file:
1. @EnableRedisHttpSession
2. public class SessionConfig
3. extends AbstractHttpSessionApplicationInitializer {
4. }
44
Note that for the gateway service, we need to use a different annotation,
namely @EnableRedisWebSession.
Finally, we’ll add these properties to the three *.properties files in our git
repository:
spring.redis.host=localhost
spring.redis.port=6379
45
3. Securing Config Service
This will set up our service to login with discovery. In addition, we’re
configuring our security with the application.properties file.
46
4. Securing Discovery Service
If malicious clients gain access, they’ll learn the network location of all the
services in our system, and be able to register their own malicious services
into our application. It’s critical that the discovery service is secured.
Let’s add a security filter to protect the endpoints the other services will
use:
1. @Configuration
2. @EnableWebSecurity
3. public class SecurityConfig {
4.
5.
6. @Autowired
7. public void configureGlobal(AuthenticationManagerBuilder auth)
8. throws Exception {
9. auth.inMemoryAuthentication().withUser(“discUser”)
10. .password(“{noop}discPassword”).roles(“SYSTEM”);
11. }
12. @Bean
13. public SecurityFilterChain securityFilterChain(HttpSecurity
14. http) throws Exception {
15. http.csrf(AbstractHttpConfigurer::disable)
16. .sessionManagement(sessionManagement ->
17. sessionManagement.
18. sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
19. .authorizeHttpRequests(authorizeRequests ->
20. authorizeRequests
47
21. .requestMatchers(HttpMethod.GET, “/eureka/**”)
22. .hasRole(“SYSTEM”)
23. .requestMatchers(HttpMethod.POST, “/
24. eureka/**”)
25. .hasRole(“SYSTEM”)
26. .requestMatchers(HttpMethod.PUT, “/
27. eureka/**”)
28. .hasRole(“SYSTEM”)
29. .requestMatchers(HttpMethod.DELETE, “/
30. eureka/**”)
31. .hasRole(“SYSTEM”)
31. .anyRequest().authenticated())
32. .httpBasic(Customizer.withDefaults());
33. return http.build();
34. }
35. }
This will set up our service with a ‘SYSTEM‘ user. This is a basic Spring
Security configuration with a few twists. Let’s take a look at those twists:
48
1. @Configuration
2. public static class AdminSecurityConfig {
3.
4.
5. public void configureGlobal(AuthenticationManagerBuilder auth)
6. throws Exception {
7. auth.inMemoryAuthentication();
8. }
9.
10.
11. protected void configure(HttpSecurity http) throws Exception {
12. http.sessionManagement(session ->
13. session.
14. sessionCreationPolicy(SessionCreationPolicy.NEVER))
15. .httpBasic(basic -> basic.disable())
16. .authorizeRequests()
17. .requestMatchers(HttpMethod.GET, “/”).hasRole(“ADMIN”)
18. .requestMatchers(“/info”, “/health”).authenticated()
19. .anyRequest().denyAll()
20. .and().csrf(csrf -> csrf.disable());
21. }
22. }
We’ll add this configuration class within the SecurityConfig class. This will
create a second security filter that will control access to our UI. This filter
has a few unusual characteristics:
This filter will never set a user session, and relies on Redis to populate a
shared security context. As such, it’s dependent on another service, the
gateway, to provide authentication.
49
4.3. Authenticating With Config Service
These properties will let the discovery service authenticate with the config
service on startup.
Now let’s commit the file to the git repository; otherwise, the changes won’t
be detected.
50
5. Securing Gateway Service
Our gateway service is the only piece of our application we want to expose
to the world. As such, it’ll need security to ensure that only authenticated
users can access sensitive information.
Let’s create a SecurityConfig class like our discovery service, and overwrite
the methods with this content:
1. @EnableWebFluxSecurity
2. @Configuration
3. public class SecurityConfig {
4. @Bean
5. public MapReactiveUserDetailsService userDetailsService() {
6. UserDetails user = User.withUsername(“user”)
7. .password(passwordEncoder().encode(“password”))
8. .roles(“USER”)
9. .build();
10. UserDetails adminUser = User.withUsername(“admin”)
11. .password(passwordEncoder().encode(“admin”))
12. .roles(“ADMIN”)
13. .build();
14. return new MapReactiveUserDetailsService(user, adminUser);
15. }
16. @Bean
17. public SecurityWebFilterChain
18. springSecurityFilterChain(ServerHttpSecurity http) {
19. http.formLogin()
20. .authenticationSuccessHandler(
21. new RedirectServerAuthenticationSuccessHandler(“/
22. home/index.html”))
51
23. .and().authorizeExchange()
24. .pathMatchers(“/book-service/**”, “/rating-
25. service/**”, “/login*”, “/”)
26. .permitAll()
27. .pathMatchers(“/eureka/**”).hasRole(“ADMIN”)
28. .anyExchange().authenticated().and()
29. .logout().and().csrf().disable().
30. httpBasic(withDefaults());
31. return http.build();
32. }
33. }
1. @Configuration
2. @EnableRedisWebSession
3. public class SessionConfig {}
The Spring Cloud Gateway filter will automatically grab the request as it’s
redirected after login, and add the session key as a cookie in the header.
This will propagate authentication to any backing service after login.
52
5.2. Authenticating With Config and Discovery Service
Let’s commit the file to the Git repository; otherwise, the changes won’t be
detected.
53
6. Securing Book Service
To secure our book service, we’ll copy the SecurityConfig class from the
gateway and overwrite the method with this content:
1. @EnableWebSecurity
2. @Configuration
3. public class SecurityConfig {
4.
5.
6. @Autowired
7. public void registerAuthProvider(AuthenticationManagerBuilder
8. auth) throws Exception {
9. auth.inMemoryAuthentication();
10. }
11.
12.
13. @Bean
14. public SecurityFilterChain filterChain(HttpSecurity http)
15. throws Exception {
16. return http.authorizeHttpRequests((auth) ->
17. auth.requestMatchers(HttpMethod.GET, “/books”)
18. .permitAll()
19. .requestMatchers(HttpMethod.GET, “/books/*”)
20. .permitAll()
21. .requestMatchers(HttpMethod.POST, “/books”)
22. .hasRole(“ADMIN”)
23. .requestMatchers(HttpMethod.PATCH, “/books/*”)
54
24. .hasRole(“ADMIN”)
25. .requestMatchers(HttpMethod.DELETE, “/books/*”)
26. .hasRole(“ADMIN”))
27. .csrf(csrf -> csrf.disable())
28. .build();
29. }
30. }
6.2. Properties
Remember to commit these changes so the book-service will pick them up.
55
7. Securing Rating Service
To secure our rating service, we’ll copy the SecurityConfig class from the
gateway and overwrite the method with this content:
1. @EnableWebSecurity
2. @Configuration
3. public class SecurityConfig {
4.
5.
6. @Bean
7. public UserDetailsService users() {
8. return new InMemoryUserDetailsManager();
9. }
10.
11.
12. @Bean
13. public SecurityFilterChain filterChain(HttpSecurity
14. httpSecurity) throws Exception {
15. return httpSecurity.authorizeHttpRequests((auth) ->
16. auth.requestMatchers(“^/ratings\\?bookId.*$”)
17. .authenticated()
18. .requestMatchers(HttpMethod.POST, “/ratings”)
19. .authenticated()
20. .requestMatchers(HttpMethod.PATCH, “/ratings/*”)
21. .hasRole(“ADMIN”)
22. .requestMatchers(HttpMethod.DELETE, “/ratings/*”)
23. .hasRole(“ADMIN”)
24. .requestMatchers(HttpMethod.GET, “/ratings”)
25. .hasRole(“ADMIN”)
26. .anyRequest()
56
27. .authenticated())
28. .httpBasic(Customizer.withDefaults())
29. .csrf(csrf -> csrf.disable())
30. .build();
31. }
32. }
7.2. Properties
Remember to commit these changes so the rating service will pick them
up.
57
8. Running and Testing
Let’s start Redis and all the services for the application: config, discovery,
gateway, book-service, and rating-service. Now let’s test!
First, we’ll create a test class in our gateway project, and create a method
for our test:
Next, we’ll set up our test and validate that we can access our unprotected
/book-service/books resource by adding this code snippet inside our test
method:
TestRestTemplate testRestTemplate = new TestRestTemplate();
String testUrl = “http://localhost:8080”;
We’ll run this test and verify the results. If we see failures, we need
to confirm that the entire application started successfully, and that
configurations were loaded from our configuration git repository.
Now let’s test that our users will be redirected to log in when visiting a
protected resource as an unauthenticated user by appending this code to
the end of the test method:
58
01. response = testRestTemplate
02. .getForEntity(testUrl + “/home/index.html”, String.class);
03. Assert.assertEquals(HttpStatus.FOUND, response.getStatusCode());
04. Assert.assertEquals(“http://localhost:8080/login”, response.
05. getHeaders()
06. .get(“Location”).get(0))
Next, let’s actually log in, and then use our session to access the user
protected result:
Now, let’s extract the session from the cookie, and propagate it to the
following request:
59
Let’s run the test again to confirm the results.
Now let’s try to access the admin section with the same session:
Now we’ll run the test again, and as expected, we’re restricted from
accessing admin areas as a plain old user.
The next test will validate that we can log in as the admin and access the
admin protected resource:
01. form.clear();
02. form.add(“username”, “admin”);
03. form.add(“password”, “admin”);
04. response = testRestTemplate
05. .postForEntity(testUrl + “/login”, form, String.class);
06.
07. sessionCookie = response.getHeaders().get(“Set-Cookie”).get(0).
08. split(“;”)[0];
09. headers = new HttpHeaders();
10. headers.add(“Cookie”, sessionCookie);
11. httpEntity = new HttpEntity<>(headers);
12.
13. response = testRestTemplate.exchange(testUrl + “/rating-service/
14. ratings/all”,
15. HttpMethod.GET, httpEntity, String.class);
16. Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
17. Assert.assertNotNull(response.getBody());
60
Our test is getting big, but we can see when we run it that by logging in as
the admin, we gain access to the admin resource.
Our final test is accessing our discovery server through our gateway. To do
this, we’ll add this code to the end of our test:
Let’s run this test one last time to confirm that everything is working.
Success!
We’re now able to log in on our gateway service and view content on
our book, rating, and discovery services without having to log in on four
separate servers!
61
9. Conclusion
Security in the cloud certainly becomes more complicated. But with the
help of Spring Security and Spring Session, we can easily solve this critical
issue.
We now have a cloud application with security around our services. Using
Spring Cloud Gateway and Spring Session, we can log users into only one
service and propagate that authentication to our entire application. This
means we can easily break our application into proper domains and secure
each of them as we see fit.
62
4. Spring Cloud – Tracing Services With Zipkin
63
1. Overview
In this chapter, we’re going to add Zipkin to our spring cloud project. Zipkin
is an open-source project that provides mechanisms for sending, receiving,
storing, and visualizing traces. This allows us to correlate activity between
servers, and get a much clearer picture of exactly what’s happening in our
services.
Note: Zipkin project has deprecated the custom server. It’s no longer
possible to run a custom Zipkin server compatible with Spring Cloud or
Spring Boot. The best approach to run a Zipkin server is inside a docker
container.
In section 2, we’ll describe how to set up Zipkin for a custom server build
(deprecated).
64
2. Zipkin Service With Custom Server Build
Our Zipkin service will serve as the store for all our spans. Each span is sent
to this service and collected into traces for future identification.
2.1. Setup
We’ll create a new Spring Boot project and add these dependencies to
pom.xml:
01. <dependency>
02. <groupId>io.zipkin.java</groupId>
03. <artifactId>zipkin-server</artifactId>
04. </dependency>
05. <dependency>
06. <groupId>io.zipkin.java</groupId>
07. <artifactId>zipkin-autoconfigure-ui</artifactId>
08. <scope>runtime</scope>
09. </dependency>
For reference: we can find the latest version on Maven Central (zipkin-server,
zipkin-autoconfigure-ui). Versions of the dependencies are inherited from
spring-boot-starter-parent.
To enable the Zipkin server, we must add some annotations to the main
application class:
01. @SpringBootApplication
02. @EnableZipkinServer
03. public class ZipkinApplication {...}
The new annotation, @EnableZipkinServer, will set up this server to listen for
incoming spans and act as our UI for querying.
65
2.3. Configuration
01. spring.cloud.config.name=zipkin
02. spring.cloud.config.discovery.service-id=config
03. spring.cloud.config.discovery.enabled=true
04. spring.cloud.config.username=configUser
05. spring.cloud.config.password=configPassword
06.
07.
08. eureka.client.serviceUrl.defaultZone=
09. http://discUser:discPassword@localhost:8082/eureka/
Now let’s add a configuration file to our config repo, located at c:\Users\
{username}\ on Windows, or /home/{username}/ on *nix.
In this directory, we’ll add a file named zipkin.properties and add these
contents:
01. spring.application.name=zipkin
02. server.port=9411
03. eureka.client.region=default
04. eureka.client.registryFetchIntervalSeconds=5
05. logging.level.org.springframework.web=debug
66
2.4. Run
The setup for the resource servers is pretty simple. In the following
sections, we’ll detail how to set up the book-service. After that, we can
configure the same for the rating-service and gateway-service.
2.5.1. Setup
To begin sending spans to our Zipkin server, we’ll add this dependency to
our pom.xml file:
01. <dependency>
02. <groupId>org.springframework.boot</groupId>
03. <artifactId>spring-boot-starter-actuator</artifactId>
04. </dependency>
67
05. <dependency>
06. <groupId>io.zipkin.reporter2</groupId>
07. <artifactId>zipkin-reporter-brave</artifactId>
08. </dependency>
09. <dependency>
10. <groupId>io.micrometer</groupId>
11. <artifactId>micrometer-tracing-bridge-brave</artifactId>
12. </dependency>
2.5.2. Configuration
68
3. Configuration With Default Server Build
In our IDE project, we’ll create a “Zipkin” folder and add a docker-compose.
yml file with the contents:
version: “3.9”
services:
zipkin:
image: openzipkin/zipkin
ports:
- 9411:9411
3.3. Run
First, we’ll start the Redis server, config, discovery, gateway, book, rating,
and Zipkin docker image with “docker compose up -d“. Then we’ll navigate
to http://localhost:8080/book-service/books address. After that, we’ll open
a new tab and navigate to http://localhost:9411. On this page, we’ll select
book-service, and press the ‘Find Traces’ button. We should see a trace
appear in the search results. Finally, we can click that trace of opening it:
On the trace page, we can see the request broken down by service. The
first two spans are created by the gateway, and the last is created by the
book-service. This shows us how much time the request spent processing
on the book-service, 18.379 ms, and on the gateway, 87.961 ms.
69
4. Conclusion
We’ve seen how easy it is to integrate Zipkin into our cloud application.
70
5. Spring Cloud – Adding Angular 4
71
1. Overview
In our last Spring Cloud chapter, we added Zipkin support into our
application. In this chapter, we’re going to be adding a front-end application
to our stack.
Up until now, we’ve been working entirely on the back end to build our
cloud application. But what good is a web app if there’s no UI? In this
chapter, we’ll solve that issue by integrating a single page application into
our project.
We’ll be writing this app using Angular and Bootstrap. The style of Angular
code feels a lot like coding a Spring app, which is a natural crossover for
a Spring developer. While the front end code will be using Angular, the
content of this chapter can be easily extended to any front end framework
with minimal effort.
72
2. Gateway Changes
With the front end in place, we’re going to switch to form based login, and
secure parts of the UI to privileged users. This requires making changes to
our gateway security configuration.
First, let’s update the configure (HttpSecurity http) method in our gateway
SecurityConfig.java class:
01. @Bean
02. public SecurityWebFilterChain filterChain(ServerHttpSecurity
03. http) {
04. http.formLogin()
05. .authenticationSuccessHandler(
06. new RedirectServerAuthenticationSuccessHandler(“/
07. home/browser/index.html”))
08. .and().authorizeExchange()
09. .pathMatchers(“/book-service/**”, “/rating-service/**”,
10. “/login*”, “/”).permitAll()
11. .pathMatchers(“/eureka/**”).hasRole(“ADMIN”)
12. .anyExchange().authenticated()
13. .and().logout().and().csrf().disable()
14. .httpBasic(withDefaults());
15. return http.build();
16. }
Then we removed the logout success URL, as the default redirect back to
the login page will work fine.
73
2.2. Add a Principal Endpoint
Now let’s add an endpoint to return the authenticated user. This will be
used in our Angular app to log in and identify the roles our user has.
This will help us control what actions they can do on our site.
01. @RestController
02. public class AuthenticationController {
03.
04. @GetMapping(“/me”)
05. public Principal getMyUser(Principal principal) {
06. return principal;
07. }
08. }
The controller returns the currently logged in user object to the caller.
This gives us all the information we need to control our Angular app.
Let’s add a very simple landing page so that users see something when
they go to the root of our application.
74
01. <!DOCTYPE html>
02. <html lang=”en”>
03. <head>
04. <meta charset=”UTF-8”>
05. <title>Book Rater Landing</title>
06. </head>
07. <body>
08. <h1>Book Rater</h1>
09. <p>So many great things about the books</p>
10. <a href=”/login”>Login</a>
11. </body>
12. </html>
75
3. Angular CLI and the Starter Project
Before starting a new Angular project, let’s make sure to install the latest
versions of Node.js and npm.
To begin, we’ll need to use npm to download and install the Angular
command line interface. Let’s open a terminal and run:
npm install -g @angular/cli
While still in the terminal, let’s navigate to the gateway project and go into
the gateway/src/main folder. We’ll create a directory called “angular” and
navigate to it.
Be patient; the CLI’s setting up a brand new project and downloading all the
JavaScript dependencies with npm. It’s not uncommon for this process to
take many minutes.
The ng command is the shortcut for the Angular CLI, the new parameter
instructs that CLI to create a new project, and the ui command gives our
project a name.
Once the new command is complete, we’ll navigate to the ui folder that was
created and run:
76
ng serve
Now let’s use npm to install bootstrap. From the ui directory, we’ll run this
command:
npm install [email protected] --save
In the ui directory, we’ll open the angular.json file. This is the file that
configures some properties about our project. Let’s find the projects > styles
property, and add a file location of our Bootstrap CSS class:
01. “styles”: [
02. “styles.css”,
03. “../node_modules/bootstrap/dist/css/bootstrap.min.css”
04. ],
This will instruct Angular to include Bootstrap in the compiled CSS file that’s
built with the project.
77
3.5. Set the Build Output Directory
Next, we need to tell Angular where to put the build files so that our spring
boot app can serve them. Spring Boot can serve files from two locations in
the resources folder:
src/main/resources/static
src/main/resource/public
Since we’re already using the static folder to serve some resources for
Eureka, and Angular deletes this folder each time a build is run, let’s build
our Angular app into the public folder.
Let’s open the angular.json file again, and find the options > outputPath
property. We’ll update that string:
“outputPath”: “../../resources/static/home”,
Lastly, we’ll set up an automated build to run when we compile our code.
This ant task will run the Angular CLI build task whenever “mvn compile” is
run. We’ll add this step to the gateway’s POM.xml to ensure that each time
we compile, we get the latest ui changes:
78
01. <plugin>
02. <artifactId>maven-antrun-plugin</artifactId>
03. <executions>
04. <execution>
05. <phase>generate-resources</phase>
06. <configuration>
07. <tasks>
08. <exec executable=”cmd” osfamily=”windows”
09. dir=”${project.basedir}/src/main/angular/
10. ui”>
11. <arg value=”/c”/>
12. <arg value=”ng”/>
13. <arg value=”build”/>
14. </exec>
15. <exec executable=”/bin/sh” osfamily=”mac”
16. dir=”${project.basedir}/src/main/angular/
17. ui”>
18. <arg value=”-c”/>
19. <arg value=”ng build”/>
20. </exec>
21. </tasks>
22. </configuration>
23. <goals>
24. <goal>run</goal>
25. </goals>
26. </execution>
27. </executions>
28. </plugin>
We should note that this setup does require that the Angular CLI be
available on the classpath. Pushing this script to an environment that
doesn’t have that dependency will result in build failures.
79
4. Angular
Users have a login form where they can enter their username and
password.
Next, we’ll use their credentials to create a base64 authentication token,
and request the “/me” endpoint. The endpoint returns a Principal object
containing the roles of this user.
Finally, we’ll store the credentials and the principal on the client to use in
subsequent requests.
4.1. Template
Here, we’re going to add some code to display a navigation bar with a login
form:
80
12. <div class=”collapse navbar-collapse” id=”navbarCollapse”>
13. <ul class=”navbar-nav mr-auto”>
14. </ul>
15. <button *ngIf=”principal.authenticated” type=”button”
16. class=”btn btn-link” (click)=”onLogout()”>Logout</button>
17. </div>
18. </nav>
19.
20. <div class=”jumbotron”>
21. <div class=”container”>
22. <h1>Book Rater App</h1>
23. <p *ngIf=”!principal.authenticated” class=”lead”>
24. Anyone can view the books.
25. </p>
26. <p *ngIf=”principal.authenticated && !principal.
27. isAdmin()” class=”lead”>
28. Users can view and create ratings</p>
29. <p *ngIf=”principal.isAdmin()” class=”lead”>Admins can
30. do anything!</p>
31. </div>
32. </div>
Next, let’s code up the Typescript file that will support this template.
81
4.2. Typescript
From the same directory, let’s open the app.component.ts file. In this file
we’ll add all the typescript properties and methods required to make our
template function:
82
31. }, (error) => {
32. console.log(error);
33. });
34. }
35.
36.
37. onLogout() {
38. this.httpService.logout()
39. .subscribe((response) => {
40. if (response.status === 200) {
41. this.loginFailed = false;
42. this.principal = new Principal(false, []);
43. window.location.replace(response.url);
44. }
45. }, (error) => {
46. console.log(error);
47. });
48. }
49. }
This class hooks into the Angular life cycle method, ngOnInit(). In this
method, we’ll call the /me endpoint to get the user’s current role and state.
This determines what the user sees on the main page. This method will
be fired whenever this component is created, which is a great time to be
checking the user’s properties for permissions in our app.
We also have an onLogout() method that logs our user out, and restores the
state of this page to its original settings.
There’s some magic going on here with the httpService property that’s
declared in the constructor. Angular is injecting this property into our class
at runtime. Angular manages singleton instances of service classes, and
injects them using constructor injection, just like Spring.
83
4.3. HttpService
In the same directory, let’s create a file named “http.service.ts”. In this file,
we’ll add this code to support the login and logout methods:
84
In this class, we’re injecting another dependency using Angular’s DI
construct. This time it’s the HttpClient class. This class handles all HTTP
communication and is provided to us by the framework.
Now we need to do one more thing to get the HttpService registered in the
dependency injection system. We’ll open the app.component.ts file and
find the providers property. Then we’ll add the HttpService to that array. The
result should look like this:
providers: [HttpService],
Next, let’s add our Principal DTO object in our Typescript code. In the same
directory, we’ll add a file called “principal.ts” and add this code:
85
14. isAdmin() {
15. return this.authorities.some(
16. (auth: Authority) => auth.authority.indexOf(‘ADMIN’)
17. > -1)
18. }
19. }
20.
21.
22. export class Authority {
23. public authority: String;
24.
25.
26. constructor(authority: String) {
27. this.authority = authority;
28. }
29. }
We added the Principal class and an Authority class. These are two DTO
classes, much like POJOs in a Spring app. Because of that, we don’t need to
register these classes with the DI system in angular.
Next, let’s configure a redirect rule to redirect unknown requests to the root
of our application.
86
4.5. 404 Handling
Now let’s navigate back into the Java code for the gateway service.
Where the GatewayApplication class resides, we’ll add a new class called
ErrorPageConfig:
01. @Component
02. public class ErrorPageConfig implements ErrorPageRegistrar {
03.
04. @Override
05. public void registerErrorPages(ErrorPageRegistry registry)
06. {
07. registry.addErrorPages(new ErrorPage(HttpStatus.NOT_
08. FOUND,
09. “/home/index.html”));
10. }
11.
12.
13. }
This class will identify any 404 response, and redirect the user to “/home/
index.html”. In a single page app, this is how we handle all traffic not going
to a dedicated resource, since the client should be handling all navigable
routes.
Now we’re ready to fire this app up and see what we built.
Let’s run “mvn compile” from the gateway folder. This will compile our
java source, and build the Angular app to the public folder. Next, let’s
start the other cloud applications: config, discovery, and zipkin. Then we’ll
run the gateway project. When the service starts, we’ll navigate to http://
localhost:8080 to see our app. We should see something like this:
87
Next, let’s follow the link to the login page:
It looks like our jumbotron is indicating we’re logged in as a user. Now we’ll
log out by clicking the link in the upper right corner, and log in using the
admin/admin credentials this time:
88
5. Conclusion
Using these examples, try to write some code to make a call to the book-
service or rating-service. Since we now have examples of making HTTP
calls and wiring data to the templates, this should be relatively easy.
If you would like to see how the rest of the site is built, as always, you can
find the source code over on Github.
89