Configurando o Spring Boot Admin: Server e Client
Tags: springboot, java, client, server
Published: 2023-04-08 02:28:06

Existem várias formas de se monitorar sistemas distribuídos e uma que me agrada bastante pela simplicidade de configuração e confiabilidade é o Spring Boot Admin.

Nesse tutorial criaremos duas aplicações utilizando o Spring Boot, uma que será o servidor de monitoramento e a outra, o cliente que deverá se registrar para ser monitorado. 

Também vamos aproveitar para implementar uma camada de segurança utilizando o Spring Security e com o #Maven, poderemos fazer o build das aplicações para serem executadas individualmente.

## Versões utilizadas

- Spring Boot: 2.7.10
- Spring Boot Admin Server: 2.7.10
- Spring Boot Admin Client: 2.7.10

## Configurando o Servidor

Crie um projeto utilizando o [Spring Initilizr](https://start.spring.io/) com as seguintes dependências:

- Starter Web
- Starter Security
- Admin Starter Server


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8oyux0tnsibtnw4s5wbx.png)

Confira se as dependências relacionadas ao Spring Boot Admin foram adicionadas:
```
...
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-server-ui</artifactId>
</dependency>
...
```

Adicione a anotação @EnableAdminServer em alguma classe de configuração:

```
...

@EnableAdminServer
@SpringBootApplication
public class SpringBootAdminServerApplication {

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

}

```

Configure o comportamento da aplicação utilizando o arquivo application.yaml. Como adicionamos uma camada de segurança, devemos criar um nome de usuário e senha para logarmos no servidor e também será necessário configurar o login para comunicação entre servidor e cliente.

```
server:
  port: 8081
  servlet:
    context-path: /admin-console
spring:
  security:
    user:
      # Configura o login do servidor.
      name: ${SBA_SERVER_USERNAME}
      password: ${SBA_SERVER_PASSWORD}
  boot:
    admin:
      client:
        # Necessários para que o cliente possa se registrar na api do servidor protegido.
        username: ${SBA_SERVER_USERNAME}
        password: ${SBA_SERVER_PASSWORD}
        instance:
          metadata:
            user:
              # Necessários para que o servidor possa acessar os endpoints protegidos do cliente.
              name: ${SBA_CLIENT_USERNAME}
              password: ${SBA_CLIENT_PASSWORD}

# LOG
logging:
  file:
    name: ${user.home}/logs/admin/sba-server.log
  level:
    root: info
    web: info
    dev.marksduarte: info
    org.springframework: info
  charset:
    file: utf-8

```
Agora vamos configurar o Spring Security criando uma classe e desabilitando o proxy dos beans na anotação @Configuration(proxyBeanMethods = false), pois como vamos trabalhar com @Bean autocontido, podemos evitar o processamento da subclasse CGLIB.

Também vamos permitir os acessos às rotas de **login** e **assets** e desabilitar a proteção **CSRF** paras o métodos **HTTP POST** e **HTTP DELETE** nas instâncias dos clientes.

```
package dev.marksduarte.springbootadminserver;

import de.codecentric.boot.admin.server.config.AdminServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration(proxyBeanMethods = false)
public class SecurityConfig {

    private final AdminServerProperties adminServer;

    public SecurityConfig(AdminServerProperties adminServer) {
        this.adminServer = adminServer;
    }

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setTargetUrlParameter("redirectTo");
        successHandler.setDefaultTargetUrl(this.adminServer.path("/"));

        http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/assets/**")))
                        .permitAll()
                        .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/login")))
                        .permitAll()
                        .anyRequest()
                        .authenticated())
                .formLogin(formLogin -> formLogin.loginPage(this.adminServer.path("/login"))
                        .successHandler(successHandler))
                .logout(logout -> logout.logoutUrl(this.adminServer.path("/logout")))
                .httpBasic(Customizer.withDefaults())
                .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        .ignoringRequestMatchers(
                                new AntPathRequestMatcher(this.adminServer.path("/instances"), HttpMethod.POST.toString()),
                                new AntPathRequestMatcher(this.adminServer.path("/instances/*"), HttpMethod.DELETE.toString()),
                                new AntPathRequestMatcher(this.adminServer.path("/actuator/**"))));

        return http.build();
    }
}

```


Pronto, agora é só rodar a aplicação e conferir se o Admin Server está acessível através do endereço http://localhost:8081/admin-console e logar com o usuário e senha informados na configuração.
  
## Configurando o Cliente

Crie um projeto utilizando o [Spring Initilizr](https://start.spring.io/) com as seguintes dependências:

- Starter Web
- Starter Actuator
- Starter Security
- Admin Starter Client

Confira no seu arquivo pom.xml, se a dependência do Spring Boot Admin Client e Spring Actuator foram adicionadas:

```
...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.7.10</version>
</dependency>
...
```

Agora vamos configurar o sistema começando pelo arquivo application.yaml:

```
## INFO ENDPOINT
## Aqui configuramos as informações sobre o sistema, como nome, descrição, versão e etc.
info:
  name: Spring Boot Admin Client
  description: Sistema Cliente
  version: @project.version@

server:
  port: 8080
  servlet:
    context-path: /admin-client

spring:
  # Configuração básica do Spring Security.
  security:
    user:
      name: ${SBA_CLIENT_USERNAME}
      password: ${SBA_CLIENT_PASSWORD}
  boot:
    admin:
      client:
        enabled: true
        # URL do servidor que o cliente deve se registrar.
        url: http://localhost:8081/admin-console
        username: ${SBA_SERVER_USERNAME}
        password: ${SBA_SERVER_PASSWORD}
        instance:
          # URL base para calcular o service-url com o qual se registrar. O caminho é inferido em tempo de execução e anexado à url base.
          service-base-url: http://localhost:8080
          # Essas informações são passadas ao servidor para que ele possa fazer o acesso aos endpoints do sistema cliente.
          metadata:
            user:
              name: ${SBA_SERVER_USERNAME}
              password: ${SBA_SERVER_PASSWORD}
        auto-deregistration: true

## APP
app:
  cors-origins:
    - http://localhost
  cors-methods:
    - GET
    - POST
    - PUT
    - DELETE
    - OPTIONS
  cors-headers:
    - Authorization
    - Content-Type
    - Content-Length
    - X-Requested-With

## ACTUATOR
management:
  info:
    env:
      # Desde o Spring Boot 2.6, o env info é desabilitado por padrão.
      enabled: true
  endpoint:
    health:
      show-details: ALWAYS
      enabled: true
    shutdown:
      enabled: true
    logfile:
      enabled: true
      external-file: logs/sba-client.log
  endpoints:
    web:
      exposure:
        # Liberamos todos os endpoints, mas lembre-se, em produção não se deve fazer isso.
        include: "*"
      cors:
        allowed-headers: ${app.cors-headers}
        allowed-methods: ${app.cors-methods}
        allowed-origins: ${app.cors-origins}

## LOG
logging:
  file:
    name: logs/sba-client.log
    path: logs
  level:
    root: info
    web: info
    dev.marksduarte: info
  charset:
    file: utf-8
  logback:
    rollingpolicy:
      clean-history-on-start: true
      max-file-size: 10MB

```
Para simplificar, vamos habilitar todas as requisições para os endpoints do "/actuator/**":

```
...

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .authorizeHttpRequests()
                .antMatchers("/actuator/**")
                .permitAll();
        return http.build();
    }
}
```

Bom, isso já é suficiente para registar nossa aplicação cliente no servidor.

Mas caso aconteça alguma exceção do tipo: _HttpMessageNotWritableException_ ou um response error HTTP 416 ao tentar acessar o arquivo de log, não se assuste, isso pode acontecer caso seu sistema tenha alguma classe de configuração do Jackson que estenda _WebMvcConfigurationSupport_. 

Nesse caso, essa classe pode estar desativando a instanciação dos _Beans_ com as configurações padrões do Spring Boot.

Para corrigir esse tipo de problema, podemos criar um Bean customizado e substituir a configuração padrão criada na inicialização do sistema.

```
@Configuration
public class JacksonConfig extends WebMvcConfigurationSupport {

    private final Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();

    private static final List<MediaType> MEDIA_TYPE_LIST = List.of(
            MediaType.ALL,
            MediaType.parseMediaType("application/vnd.spring-boot.actuator.v2+json")
    );

    @Bean
    public MappingJackson2HttpMessageConverter customMappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = actuatorConverter();
        converter.setSupportedMediaTypes(MEDIA_TYPE_LIST);
        return converter;
    }

    private MappingJackson2HttpMessageConverter actuatorConverter() {
        return new MappingJackson2HttpMessageConverter(builder.build()) {
            @Override
            protected boolean canWrite(MediaType mediaType) {
                // O método super, retorna true se for null.
                // Assim evitamos a exceção _HttpMessageNotWritableException_ caso o Content-Type 'null' seja enviado.
                return mediaType != null && super.canWrite(mediaType);
            }
        };
    }

    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        /*
        Remove somente o MappingJackson2HttpMessageConverter
padrão e substitui pelo customMappingJackson2HttpMessageConverter.
         */
        var defaultHttpConverterOpt = converters.stream()
                .filter(MappingJackson2HttpMessageConverter.class::isInstance)
                .findFirst();

        defaultHttpConverterOpt.ifPresent(converters::remove);
        converters.add(customMappingJackson2HttpMessageConverter());
    }
}
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73vncstpk358zck8vevt.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yqr410afl7poewinad5z.png)

Código disponível em [GitHub Marks Duarte](https://github.com/marksduarte/spring-boot-admin)

Por enquanto é só. Até mais! ;)