ToB企服应用市场:ToB评测及商务社交产业平台
标题:
SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告
[打印本页]
作者:
用多少眼泪才能让你相信
时间:
2022-9-2 10:19
标题:
SpringCloud微服务实战——搭建企业级开发框架(四十四):【微服务监控告
业务系统正常运行的稳定性十分重要,作为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基础平台包中,这样所有的微服务都集成了此功能。
<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.version>2.3.1</spring.boot.admin.version>
......
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${spring.boot.admin.version}</version>
</dependency>
<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>
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-boot</artifactId>
<exclusions>
<exclusion>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-cache</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-cloud</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<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">GitEgg微服务监控系统
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权限验证器。
[code]/** * 权限配置 * 注解需要使用@EnableWebFluxSecurity而非@EnableWebSecurity,因为SpringCloud Gateway基于WebFlux * * @author GitEgg * */@RequiredArgsConstructor(onConstructor_ = @Autowired)@Configuration@EnableWebFluxSecuritypublic 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
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4