SpringCloud微服務實戰——搭建企業級開發框架(四十四):【微服務監控告警實現方式一】使用Actuator + Spring Boot Admin實現簡單的微服務監控告警系統

2022-07-27 15:01:20

  業務系統正常執行的穩定性十分重要,作為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上有版本對應關係的說明:

  我們在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 {
        
        // 當設定了publicUrl時,Gateway跳轉到login或logout連結需要redirect到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微服務監控系統</span>
        title: GitEgg微服務監控系統
        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:
          # 啟用SpringBootAdmin時 使用者端端點資訊的安全認證資訊
          user.name: ${spring.security.user.name}
          user.password: ${spring.security.user.password}
      config:
        server-addr: ${spring.nacos.addr}
        file-extension: yaml
        extension-configs:
          # 必須帶副檔名,此時 file-extension 的設定對自定義擴充套件設定的 Data Id 副檔名沒有影響
          - 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許可權驗證器。

/**
 * 許可權設定
 * 註解需要使用@EnableWebFluxSecurity而非@EnableWebSecurity,因為SpringCloud Gateway基於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;
    
    /**
     * 健康檢查介面許可權設定
     * @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("當啟用健康檢查時,不允許健康檢查的路徑為空");
        }
        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();
    }
    
    /**
     * 設定Basic認證使用者資訊
     * @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());
    }
    
    /**
     * 設定密碼編碼
     * @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;
    }
    
    /**
     * 路由轉發許可權設定
     * @param http
     * @return
     */
    @Bean
    SecurityWebFilterChain apiHttpSecurity(ServerHttpSecurity http) {
        
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
    
        // 自定義處理JWT請求頭過期或簽名錯誤的結果
        http.oauth2ResourceServer().authenticationEntryPoint(authServerAuthenticationEntryPoint);
    
        // 對白名單路徑,直接移除JWT請求頭,不移除的話,後臺會校驗jwt
        http.addFilterBefore(whiteListRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
    
        // Basic認證直接放行
        if (!CollectionUtils.isEmpty(authUrlWhiteListProperties.getTokenUrls()))
        {
            http.authorizeExchange().pathMatchers(ArrayUtil.toArray(authUrlWhiteListProperties.getTokenUrls(), String.class)).permitAll();
        }
    
        // 判斷是否有靜態檔案
        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()
                /**
                 * 處理未授權
                 */
                .accessDeniedHandler(authServerAccessDeniedHandler)
                /**
                 * 處理未認證
                 */
                .authenticationEntryPoint(authServerAuthenticationEntryPoint)
                .and()
                .cors()
                .and().csrf().disable();
    
        return http.build();
    }
    
    /**
     * ServerHttpSecurity沒有將jwt中authorities的負載部分當做Authentication,需要把jwt的Claim中的authorities加入
     * 解決方案:重新定義ReactiveAuthenticationManager許可權管理器,預設轉換器JwtGrantedAuthoritiesConverter
     */
    @Bean
    public Converter<Jwt, ? extends Mono<? extends 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);
    }
}

6、在Nacos設定中心,統一設定所有微服務的健康檢查端點地址,許可權校驗的使用者名稱密碼等。
spring:
......
  security:
    # # 啟用SpringBootAdmin時,健康檢查許可權校驗,不使用SpringBootAdmin此處可省略
    user:
      name: user
      password: password
      roles: ACTUATOR_ADMIN
......

# 效能監控端點設定
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
......
8、啟動所有的微服務,並存取 http://127.0.0.1/gitegg-admin-monitor/monitor/login 進行健康檢查微服務設定。

  根據我們在Nacos中的設定,我們這裡的登入使用者名稱密碼是:user / password


  以上為SpringBootAdmin在SpringCloud微服務中的搭建和設定步驟,相比較而言比較簡單,但是一定要注意許可權問題,不要因為健康檢查而洩露了系統資訊。我們這裡是通過Gateway進行的統一鑑權,在生產環境部署時,一定要注意修改預設的Basic校驗使用者名稱密碼,甚至需要修改健康檢查端點。

原始碼地址:

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

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