SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统

业务系统正常运行的稳定性十分重要,作为SpringBoot的四大核心之一,Actuator让你时刻探知SpringBoot服务运行状态信息,是保障系统正常运行必不可少的组件。
spring-boot-starter-actuator提供的是一系列HTTP或者JMX监控端点,通过监控端点我们可以获取到系统的运行统计信息,同时,我们可以自己选择开启需要的监控端点,也可以自定义扩展监控端点。
Actuator通过端点对外暴露的监控信息是JSON格式数据,我们需要使用界面来展示,目前使用比较多的就是Spring Boot Admin或者Prometheus + Grafana的方式:Spring Boot Admin实现起来相对比较简单,不存在数据库,不能存储和展示历史监控数据;Prometheus(时序数据库) + Grafana(界面)的方式相比较而言功能更丰富,提供历史记录存储,界面展示也比较美观。
相比较而言,Prometheus + Grafana的方式更为流行一些,现在的微服务及Kubernetes基本是采用这种方式的。但是对于小的项目或者单体应用,Spring Boot Admin会更加方便快捷一些。具体采用那种方式,可以根据自己的系统运维需求来取舍,这里我们把框架集成两种方式,在实际应用过程中自有选择。

本文主要介绍如何集成Spring Boot Admin以及通过SpringSecurity控制Actuator的端点权限。

1、在基础服务gitegg-platform中引入spring-boot-starter-actuator包。

无论是使用Spring Boot Admin还是使用Prometheus + Grafana的方式都需要spring-boot-starter-actuator来获取监控信息,这里将spring-boot-starter-actuator包添加到gitegg-platform-boot基础平台包中,这样所有的微服务都集成了此功能。

        <!-- spring boot 健康监控 -->
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-actuator</artifactid>
        </dependency>
2、确定并引入工程使用的spring-boot-admin-starter-server和spring-boot-admin-starter-client依赖包。

spring-boot-admin-starter-server是Spring Boot Admin的服务端,我们需要新建一个SpringBoot工程来启动这个服务端,用来接收需要监控的服务注册,展示监控告警信息。spring-boot-admin-starter-client是客户端,需要被监控的服务需要引入这个依赖包。
此处请注意: 看到网上很多文章里面写着添加spring-boot-admin-starter-client包,在SpringCloud微服务中是不需要引入的,spring-boot-admin-starter-client包仅仅是为了引入我们gitegg-platform平台工程的对应版本,在gitegg-boot框架中使用,在SpringCloud微服务框架中,不需要引入spring-boot-admin-starter-client,SpringBootAdmin会自动根据微服务注册信息查找服务端点,官方文档说明:spring-cloud-discovery-support
在选择版本时,一定要找到对应SpringBoot版本的Spring Boot Admin,GitHub上有版本对应关系的说明:

SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统
我们在gitegg-platform-pom中来定义需要引入的spring-boot-admin-starter-server和spring-boot-admin-starter-client依赖包版本,然后在微服务业务开发中具体引入,这里不做统一引入,方便微服务切换监控方式。
......

        <!-- spring-boot-admin 微服务监控-->
        <spring.boot.admin.version>2.3.1</spring.boot.admin.version>
......

            <!-- spring-boot-admin监控 服务端 https://mvnrepository.com/artifact/de.codecentric/spring-boot-admin-starter-server -->
            <dependency>
                <groupid>de.codecentric</groupid>
                <artifactid>spring-boot-admin-starter-server</artifactid>
                <version>${spring.boot.admin.version}</version>
            </dependency>

            <!-- spring-boot-admin监控 客户端 https://mvnrepository.com/artifact/de.codecentric/spring-boot-admin-starter-client -->
            <dependency>
                <groupid>de.codecentric</groupid>
                <artifactid>spring-boot-admin-starter-client</artifactid>
                <version>${spring.boot.admin.version}</version>
            </dependency>.

......

3、在GitEgg-Cloud项目的gitegg-plugin工程下新建gitegg-admin-monitor工程,用于运行spring-boot-admin-starter-server。
  • pom.xml中引入需要的依赖包
    <dependencies>
        <!-- gitegg Spring Boot自定义及扩展 -->
        <dependency>
            <groupid>com.gitegg.platform</groupid>
            <artifactid>gitegg-platform-boot</artifactid>
            <!-- 去除gitegg-platform-boot默认的依赖-->
            <exclusions>
                <exclusion>
                    <groupid>com.gitegg.platform</groupid>
                    <artifactid>gitegg-platform-cache</artifactid>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- gitegg Spring Cloud自定义及扩展 -->
        <dependency>
            <groupid>com.gitegg.platform</groupid>
            <artifactid>gitegg-platform-cloud</artifactid>
        </dependency>
        <!-- security -->
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-security</artifactid>
            <!-- 去除springboot默认的logback配置-->
            <exclusions>
                <exclusion>
                    <groupid>org.springframework.boot</groupid>
                    <artifactid>spring-boot-starter-logging</artifactid>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupid>de.codecentric</groupid>
            <artifactid>spring-boot-admin-starter-server</artifactid>
        </dependency>
    </dependencies>

  • 添加spring-boot-admin-starter-server启动类GitEggMonitorApplication.java,添加@EnableAdminServer注解即可。
@EnableAdminServer
@SpringBootApplication
@RefreshScope
public class GitEggMonitorApplication {

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

}
  • 添加SpringSecurity的WebSecurityConfigurerAdapter配置类,保护监控系统安全。
    这里主要配置登录页面、静态文件、登录、退出等的权限。请注意这里配置了publicUrl的前缀,当部署在微服务环境或Docker环境中需要经过gateway或者nginx转发时,在SpringBootAdmin配置中,需要配置publicUrl,否则SpringBootAdmin只会跳转到本机环境的地址和端口。publicUrl如果是80端口,那么这个端口不能省略,需要配置上。
@Configuration(proxyBeanMethods = false)
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {

    private final AdminServerUiProperties adminUi;

    private final AdminServerProperties adminServer;

    private final SecurityProperties security;

    public SecuritySecureConfig(AdminServerUiProperties adminUi, AdminServerProperties adminServer, SecurityProperties security) {
        this.adminUi = adminUi;
        this.adminServer = adminServer;
        this.security = security;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // &#x5F53;&#x8BBE;&#x7F6E;&#x4E86;publicUrl&#x65F6;&#xFF0C;Gateway&#x8DF3;&#x8F6C;&#x5230;login&#x6216;logout&#x94FE;&#x63A5;&#x9700;&#x8981;redirect&#x5230;publicUrl
        String publicUrl = this.adminUi.getPublicUrl() != null ? this.adminUi.getPublicUrl() : this.adminServer.getContextPath();
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setTargetUrlParameter("redirectTo");
        successHandler.setDefaultTargetUrl(publicUrl + "/");

        http.authorizeRequests(
                (authorizeRequests) -> authorizeRequests.antMatchers(this.adminServer.path("/assets/**")).permitAll()
                        .antMatchers(this.adminServer.path("/actuator/info")).permitAll()
                        .antMatchers(this.adminServer.path("/actuator/health")).permitAll()
                        .antMatchers(this.adminServer.path("/login")).permitAll().anyRequest().authenticated()
        ).formLogin(
                (formLogin) -> formLogin.loginPage(publicUrl + "/login").loginProcessingUrl(this.adminServer.path("/login")).successHandler(successHandler).and()
        ).logout((logout) -> logout.logoutUrl(publicUrl + "/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/**"))
                        ))
                .rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600));
    }

    /**
     * Required to provide UserDetailsService for "remember functionality"
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser(security.getUser().getName())
                .password("{noop}" + security.getUser().getPassword()).roles(security.getUser().getRoles().toArray(new String[0]));
    }

}
4、在Nacos配置中心配置SpringBootAdmin的相关配置,在gitegg-admin-monitor工程中,也需要配置读取配置的相关yml文件,除了读取主配置之外,还需要读取SpringBootAdmin专属配置。
  • 新增gitegg-cloud-config-admin-monitor.yaml配置文件
spring:
  boot:
    admin:
      ui:
        brand: <img src="https://img2022.cnblogs.com/blog/460952/202207/460952-20220727124816822-208395561.png"><span>GitEgg&#x5FAE;&#x670D;&#x52A1;&#x76D1;&#x63A7;&#x7CFB;&#x7EDF;</span>
        title: GitEgg&#x5FAE;&#x670D;&#x52A1;&#x76D1;&#x63A7;&#x7CFB;&#x7EDF;
        favicon: https://img2022.cnblogs.com/blog/460952/202207/460952-20220727124816822-208395561.png
        public-url: http://127.0.0.1:80/gitegg-admin-monitor/monitor
      context-path: /monitor
  • 在bootstrap.yml中新增读取gitegg-cloud-config-admin-monitor.yaml的配置
server:
  port: 8009
spring:
  profiles:
    active: '@spring.profiles.active@'
  application:
    name: '@artifactId@'
  cloud:
    inetutils:
      ignored-interfaces: docker0
    nacos:
      discovery:
        server-addr: ${spring.nacos.addr}
        metadata:
          # &#x542F;&#x7528;SpringBootAdmin&#x65F6; &#x5BA2;&#x6237;&#x7AEF;&#x7AEF;&#x70B9;&#x4FE1;&#x606F;&#x7684;&#x5B89;&#x5168;&#x8BA4;&#x8BC1;&#x4FE1;&#x606F;
          user.name: ${spring.security.user.name}
          user.password: ${spring.security.user.password}
      config:
        server-addr: ${spring.nacos.addr}
        file-extension: yaml
        extension-configs:
          # &#x5FC5;&#x987B;&#x5E26;&#x6587;&#x4EF6;&#x6269;&#x5C55;&#x540D;&#xFF0C;&#x6B64;&#x65F6; file-extension &#x7684;&#x914D;&#x7F6E;&#x5BF9;&#x81EA;&#x5B9A;&#x4E49;&#x6269;&#x5C55;&#x914D;&#x7F6E;&#x7684; Data Id &#x6587;&#x4EF6;&#x6269;&#x5C55;&#x540D;&#x6CA1;&#x6709;&#x5F71;&#x54CD;
          - data-id: ${spring.nacos.config.prefix}.yaml
            group: ${spring.nacos.config.group}
            refresh: true
          - data-id: ${spring.nacos.config.prefix}-admin-monitor.yaml
            group: ${spring.nacos.config.group}
            refresh: true
5、扩展gitegg-gateway的SpringSecurity配置,增加统一鉴权校验。因我们有多个微服务,且所有的微服务在生产环境部署时都不会暴露端口,所以所有的微服务鉴权都会在网关做。

SpringSecurity权限验证支持多过滤器配置,同时可配置验证顺序,我们这里需要改造之前的过滤器,这里新增Basic认证过滤器,通过securityMatcher设置,只有健康检查的请求走这个权限过滤器,其他请求继续走之前我们设置的OAuth2+JWT权限验证器。

/**
 * &#x6743;&#x9650;&#x914D;&#x7F6E;
 * &#x6CE8;&#x89E3;&#x9700;&#x8981;&#x4F7F;&#x7528;@EnableWebFluxSecurity&#x800C;&#x975E;@EnableWebSecurity,&#x56E0;&#x4E3A;SpringCloud Gateway&#x57FA;&#x4E8E;WebFlux
 *
 * @author GitEgg
 *
 */
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Configuration
@EnableWebFluxSecurity
public class MultiWebSecurityConfig {

    private final AuthorizationManager authorizationManager;

    private final AuthServerAccessDeniedHandler authServerAccessDeniedHandler;

    private final AuthServerAuthenticationEntryPoint authServerAuthenticationEntryPoint;

    private final AuthUrlWhiteListProperties authUrlWhiteListProperties;

    private final WhiteListRemoveJwtFilter whiteListRemoveJwtFilter;

    private final SecurityProperties securityProperties;

    @Value("${management.endpoints.web.base-path:}")
    private String actuatorPath;

    /**
     * &#x5065;&#x5EB7;&#x68C0;&#x67E5;&#x63A5;&#x53E3;&#x6743;&#x9650;&#x914D;&#x7F6E;
     * @param http
     * @return
     */
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Bean
    @ConditionalOnProperty( value = {"management.security.enabled", "management.endpoints.enabled-by-default"}, havingValue = "true")
    SecurityWebFilterChain webHttpSecurity(ServerHttpSecurity http) {
        if (StringUtils.isEmpty(actuatorPath))
        {
            throw new BusinessException("&#x5F53;&#x542F;&#x7528;&#x5065;&#x5EB7;&#x68C0;&#x67E5;&#x65F6;&#xFF0C;&#x4E0D;&#x5141;&#x8BB8;&#x5065;&#x5EB7;&#x68C0;&#x67E5;&#x7684;&#x8DEF;&#x5F84;&#x4E3A;&#x7A7A;");
        }
        http
                .cors()
                .and()
                .csrf().disable()
                .formLogin().disable()
                .securityMatcher(new OrServerWebExchangeMatcher(
                        new PathPatternParserServerWebExchangeMatcher(actuatorPath + "/**"),
                        new PathPatternParserServerWebExchangeMatcher("/**" + actuatorPath + "/**")
                ))
                .authorizeExchange((exchanges) -> exchanges
                        .anyExchange().hasAnyRole(securityProperties.getUser().getRoles().toArray(new String[0]))
                )
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    /**
     * &#x8BBE;&#x7F6E;Basic&#x8BA4;&#x8BC1;&#x7528;&#x6237;&#x4FE1;&#x606F;
     * @return
     */
    @Bean
    @ConditionalOnProperty( value = {"management.security.enabled", "management.endpoints.enabled-by-default"}, havingValue = "true")
    ReactiveUserDetailsService userDetailsService() {
        return new MapReactiveUserDetailsService(User
                .withUsername(securityProperties.getUser().getName())
                .password(passwordEncoder().encode(securityProperties.getUser().getPassword()))
                .roles(securityProperties.getUser().getRoles().toArray(new String[0]))
                .build());
    }

    /**
     * &#x8BBE;&#x7F6E;&#x5BC6;&#x7801;&#x7F16;&#x7801;
     * @return
     */
    @Bean
    @ConditionalOnProperty( value = {"management.security.enabled", "management.endpoints.enabled-by-default"}, havingValue = "true")
    public static PasswordEncoder passwordEncoder() {
        DelegatingPasswordEncoder delegatingPasswordEncoder =
                (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
        return  delegatingPasswordEncoder;
    }

    /**
     * &#x8DEF;&#x7531;&#x8F6C;&#x53D1;&#x6743;&#x9650;&#x914D;&#x7F6E;
     * @param http
     * @return
     */
    @Bean
    SecurityWebFilterChain apiHttpSecurity(ServerHttpSecurity http) {

        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());

        // &#x81EA;&#x5B9A;&#x4E49;&#x5904;&#x7406;JWT&#x8BF7;&#x6C42;&#x5934;&#x8FC7;&#x671F;&#x6216;&#x7B7E;&#x540D;&#x9519;&#x8BEF;&#x7684;&#x7ED3;&#x679C;
        http.oauth2ResourceServer().authenticationEntryPoint(authServerAuthenticationEntryPoint);

        // &#x5BF9;&#x767D;&#x540D;&#x5355;&#x8DEF;&#x5F84;&#xFF0C;&#x76F4;&#x63A5;&#x79FB;&#x9664;JWT&#x8BF7;&#x6C42;&#x5934;&#xFF0C;&#x4E0D;&#x79FB;&#x9664;&#x7684;&#x8BDD;&#xFF0C;&#x540E;&#x53F0;&#x4F1A;&#x6821;&#x9A8C;jwt
        http.addFilterBefore(whiteListRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);

        // Basic&#x8BA4;&#x8BC1;&#x76F4;&#x63A5;&#x653E;&#x884C;
        if (!CollectionUtils.isEmpty(authUrlWhiteListProperties.getTokenUrls()))
        {
            http.authorizeExchange().pathMatchers(ArrayUtil.toArray(authUrlWhiteListProperties.getTokenUrls(), String.class)).permitAll();
        }

        // &#x5224;&#x65AD;&#x662F;&#x5426;&#x6709;&#x9759;&#x6001;&#x6587;&#x4EF6;
        if (!CollectionUtils.isEmpty(authUrlWhiteListProperties.getStaticFiles()))
        {
            http.authorizeExchange().pathMatchers(ArrayUtil.toArray(authUrlWhiteListProperties.getStaticFiles(), String.class)).permitAll();
        }

        http.authorizeExchange()
                .pathMatchers(ArrayUtil.toArray(authUrlWhiteListProperties.getWhiteUrls(), String.class)).permitAll()
                .anyExchange().access(authorizationManager)
                .and()
                .exceptionHandling()
                /**
                 * &#x5904;&#x7406;&#x672A;&#x6388;&#x6743;
                 */
                .accessDeniedHandler(authServerAccessDeniedHandler)
                /**
                 * &#x5904;&#x7406;&#x672A;&#x8BA4;&#x8BC1;
                 */
                .authenticationEntryPoint(authServerAuthenticationEntryPoint)
                .and()
                .cors()
                .and().csrf().disable();

        return http.build();
    }

    /**
     * ServerHttpSecurity&#x6CA1;&#x6709;&#x5C06;jwt&#x4E2D;authorities&#x7684;&#x8D1F;&#x8F7D;&#x90E8;&#x5206;&#x5F53;&#x505A;Authentication&#xFF0C;&#x9700;&#x8981;&#x628A;jwt&#x7684;Claim&#x4E2D;&#x7684;authorities&#x52A0;&#x5165;
     * &#x89E3;&#x51B3;&#x65B9;&#x6848;&#xFF1A;&#x91CD;&#x65B0;&#x5B9A;&#x4E49;ReactiveAuthenticationManager&#x6743;&#x9650;&#x7BA1;&#x7406;&#x5668;&#xFF0C;&#x9ED8;&#x8BA4;&#x8F6C;&#x6362;&#x5668;JwtGrantedAuthoritiesConverter
     */
    @Bean
    public Converter<jwt, ? extends mono<? abstractauthenticationtoken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
}

</jwt,>
6、在Nacos配置中心,统一配置所有微服务的健康检查端点地址,权限校验的用户名密码等。
spring:
......

  security:
    # # &#x542F;&#x7528;SpringBootAdmin&#x65F6;&#xFF0C;&#x5065;&#x5EB7;&#x68C0;&#x67E5;&#x6743;&#x9650;&#x6821;&#x9A8C;&#xFF0C;&#x4E0D;&#x4F7F;&#x7528;SpringBootAdmin&#x6B64;&#x5904;&#x53EF;&#x7701;&#x7565;
    user:
      name: user
      password: password
      roles: ACTUATOR_ADMIN
......

&#x6027;&#x80FD;&#x76D1;&#x63A7;&#x7AEF;&#x70B9;&#x914D;&#x7F6E;
management:
  security:
    enabled: true
    role: ACTUATOR_ADMIN
  endpoint:
    health:
      show-details: always
  endpoints:
    enabled-by-default: true
    web:
      base-path: /actuator
      exposure:
        include: '*'
  server:
    servlet:
      context-path: /actuator
  health:
    mail:
      enabled: false
......

7、设置网关Gateway配置,对gitegg-admin-monitor进行过路由和转发。
spring:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
......

        - id: gitegg-admin-monitor
          uri: lb://gitegg-admin-monitor
          predicates:
            - Path=/gitegg-admin-monitor/**
          filters:
            - StripPrefix=1
        - id: monitor
          uri: lb://gitegg-admin-monitor
          predicates:
            - Path=/monitor/**
          filters:
            - StripPrefix=0
......

根据我们在Nacos中的配置,我们这里的登录用户名密码是:user / password

SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统
SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统
SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统

以上为SpringBootAdmin在SpringCloud微服务中的搭建和配置步骤,相比较而言比较简单,但是一定要注意权限问题,不要因为健康检查而泄露了系统信息。我们这里是通过Gateway进行的统一鉴权,在生产环境部署时,一定要注意修改默认的Basic校验用户名密码,甚至需要修改健康检查端点。

源码地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

Original: https://www.cnblogs.com/FullStackProgrammer/p/16524475.html
Author: 全栈程序猿
Title: SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告警实现方式一】使用Actuator + Spring Boot Admin实现简单的微服务监控告警系统

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/590127/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球