美文网首页SpringCloudSpring Cloud
SpringCloud微服务实战——搭建企业级开发框架(四十四)

SpringCloud微服务实战——搭建企业级开发框架(四十四)

作者: 全栈程序猿 | 来源:发表于2022-07-27 12:47 被阅读0次

      业务系统正常运行的稳定性十分重要,作为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="http://img.gitegg.com/cloud/docs/images/logo.png"><span>GitEgg微服务监控系统</span>
            title: GitEgg微服务监控系统
            favicon: http://img.gitegg.com/cloud/docs/images/logo.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校验用户名密码,甚至需要修改健康检查端点。

    GitEgg-Cloud是一款基于SpringCloud整合搭建的企业级微服务应用开发框架,开源项目地址:

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

    欢迎感兴趣的小伙伴Star支持一下。

    相关文章

      网友评论

        本文标题:SpringCloud微服务实战——搭建企业级开发框架(四十四)

        本文链接:https://www.haomeiwen.com/subject/dxycprtx.html