SpringBoot2.x整合监控(2-SpringBoot Admin)

JAVA && Spring && SpringBoot2.x — 学习目录

SpringBoot Admin原理就是使用SpringBoot actuator提供的端点,可以通过HTTP访问。将得到的信息显示在页面上。需要注意的是:SpringBoot Actuator端点显示的是整个服务生命周期中的相关信息,若是应用部署在公网上,要慎重选择公开的端点。为了端点的安全性,可以引入Spring Security进行权限控制。

Spring Boot Admin 官网

1. 相关配置

1.1 [客户端]相关配置

1.1.1 pom文件

        <!--监控-客户端-->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
            <version>2.1.6</version>
        </dependency>
        <!--权限控制-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

1.1.2 配置文件

spring:
  boot:
    admin:
      client:
        url: http://localhost:7000  #监控-服务器地址
        instance:
          # service-base-url: http://127.0.0.1:8080  #自定义节点的ip地址
          prefer-ip: true  #是否显示真实的ip地址
          #元数据,用于配置monitor server访问client端的凭证
          metadata:
            user.name: user
            user.password: 123456
        #client可以连接到monitor server端的凭证
        username: admin
        password: 123456
        read-timeout: 10000  #读取超时时间
  application:
    #应用名
    name: 监控 客户端测试项目


#公开所有的端点
management:
  endpoints:
    web:
      exposure:
        #展示某些端点(默认展示health,info,其余均禁止)
        include: health,info,metrics
      # CORS跨域支持
      cors:
        allowed-origins: "*"
        allowed-methods: GET,POST

  #health端点的访问权限
  endpoint:
    health:
      #选择展示
      show-details: always
  health:
    mail:
      enabled: false  #不监控邮件服务器状态

#自定义的健康信息,使用@Message@取得的是maven中的配置信息
info:
  version: @project.version@
  groupId: @project.groupId@
  artifactId: @project.artifactId@
#显示所有的健康信息

1.1.3 安全控制

因为客户端需要暴露一些端点(SpringBoot Actuator),若是服务部署在外网上,可能会造成信息泄露,故需要使用Spring Security进行安全认证。

需要注意的是:若是加入了security的maven依赖后,会自动的对所有路径使用httpBasic安全认证。

配置认证规则和加密模式:

@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    /**
     * 自定义授权规则,只对端点进行安全访问
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests()
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().csrf();
        //同上
//        http.authorizeRequests()
//                .antMatchers("/actuator/**").authenticated()  //该url需要认证
//                .antMatchers("/**").permitAll().and().httpBasic();
//        ;
    }
    /**
     * 配置用户名密码的加密方式
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("user")
                .password(new MyPasswordEncoder().encode("123456")).roles("ADMIN");
    }
}
/**
* 加密类
**/
public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return Md5Utils.hash((String)charSequence);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(Md5Utils.hash((String)rawPassword));

    }
}

客户端在该处配置[configure(AuthenticationManagerBuilder)]后,无需在配置文件中进行Security的用户名,密码配置。即:

spring.boot.admin.client:
   username: user
   password: 123456

1.1.4 如何动态的配置参数

监控客户端,需要在配置文件中填写监控服务端的安全凭证以及客户端的安全凭证,但是将[用户名,密码]明文的配置在配置文件中,可能会造成一些安全隐患。那么如何在代码中动态的进行参数的配置呢?

@Configuration
public class AdminClientConfig {
    /**
     * 配置文件,修改SpringBoot的自动装配
     *
     * {@link SpringBootAdminClientAutoConfiguration.ServletConfiguration#applicationFactory(InstanceProperties, ManagementServerProperties, ServerProperties, ServletContext, PathMappedEndpoints, WebEndpointProperties, MetadataContributor, DispatcherServletPath)}
     *
     *
     */
    @Configuration
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
    public static class ServletConfiguration {
        @Bean
        public ApplicationFactory applicationFactory(InstanceProperties instance,
                                                     ManagementServerProperties management,
                                                     ServerProperties server,
                                                     ServletContext servletContext,
                                                     PathMappedEndpoints pathMappedEndpoints,
                                                     WebEndpointProperties webEndpoint,
                                                     MetadataContributor metadataContributor,
                                                     DispatcherServletPath dispatcherServletPath) {
            //可以获取到instance原数据,进行个性化的业务操作,例如在数据库中动态获取(密码)
            String username = instance.getMetadata().get("user.name");
            return new ServletApplicationFactory(instance,
                    management,
                    server,
                    servletContext,
                    pathMappedEndpoints,
                    webEndpoint,
                    metadataContributor,
                    dispatcherServletPath
            );
        }
    }

    /**
     * 注册的程序
     * {@link SpringBootAdminClientAutoConfiguration#registrator(ClientProperties, ApplicationFactory)}
     * @param client
     * @param applicationFactory
     * @return
     */
    @Bean
    public ApplicationRegistrator registrator(ClientProperties client, ApplicationFactory applicationFactory) {
        //设置RestTemplateBuilder参数
        RestTemplateBuilder builder = new RestTemplateBuilder().setConnectTimeout(client.getConnectTimeout())
                .setReadTimeout(client.getReadTimeout());

        if (client.getUsername() != null) {
            //获取用户名密码
            builder = builder.basicAuthentication(client.getUsername(), client.getPassword());
        }
        return new ApplicationRegistrator(builder.build(), client, applicationFactory);
    }
}

1.2 [服务端]相关配置

1.2.1 pom配置

        <!--监控服务端-->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            <version>2.1.6</version>
        </dependency>
        <!--整合安全机制-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--邮件通知-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

1.2.2 配置文件配置

spring:
  #springboot adminUi监控配置start
  application:
    name: spring-boot-admin
  boot:
    admin:
      notify:
        mail:
          enabled: false  #关闭admin自带的邮件通知
      monitor:
        read-timeout: 200000
      ui:
        title: 服务监控
  mail:
    host: smtp.example.com
    username: admin@example.com #邮箱地址
    password:  xxxxxxxxxx    #授权码
    properties:
      mail:
        smtp:
          starttls:
            enable: true
            required: true

  freemarker:
    settings:
      classic_compatible: true   #解决模板空指针问题
  #springboot adminUi监控配置end

需要注意的是:若是采用SpringBoot Admin自带的邮件通知,那么不能按照业务进行分组通知,需要我们关闭自带的邮件通知,手动进行通知。

1.2.3 自定义通知

您可以通过添加实现Notifier接口的Spring Beans来添加您自己的通知程序,最好通过扩展 AbstractEventNotifier或AbstractStatusChangeNotifier。

可参考源码自定义通知de.codecentric.boot.admin.server.notify.MailNotifier

@Component
@Slf4j
public class CustomMailNotifier extends AbstractStatusChangeNotifier {

    //自定义邮件发送类
    @Resource
    private SendEmailUtils sendEmailUtils;

    //自定义邮件模板
    private final static String email_template="updatePsw.ftl";

    private static Map<String,String> instance_name=new HashMap<>();

    static {
        instance_name.put("DOWN","服务心跳异常通知");
        instance_name.put("OFFLINE","服务下线报警通知");
        instance_name.put("UP","服务恢复通知");
        instance_name.put("UNKNOWN","服务未知异常");
    }

    public CustomMailNotifier(InstanceRepository repository) {
        super(repository);
    }

    @Override
    protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {

        return Mono.fromRunnable(() -> {

            if (event instanceof InstanceStatusChangedEvent) {
                String serviceUrl = instance.getRegistration().getServiceUrl();
                log.info("【邮件通知】-【Instance {} ({}) is {},The IP is {}】", instance.getRegistration().getName(), event.getInstance(),
                        ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus(), serviceUrl);
                //获取服务地址
                String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus();
                Map<String, Object> model = new HashMap<>();
                model.put("ipAddress", instance.getRegistration().getServiceUrl());
                model.put("instanceName", instance.getRegistration().getName());
                model.put("instanceId", instance.getId());
                model.put("startup", null);
                //邮件接收者,可根据instanceName灵活配置
                String toMail = "xxx@qq.com";
                String[] ccMail = {"xxxx@qq.com","yyy@qq.com"};
                switch (status) {
                    // 健康检查没通过
                    case "DOWN":
                        log.error(instance.getRegistration().getServiceUrl() + "服务心跳异常。");
                        model.put("status", "服务心跳异常");
                        Map<String, Object> details = instance.getStatusInfo().getDetails();
                        //遍历Map,查找down掉的服务
                        Map<String ,String> errorMap=new HashMap<>();
                        StringBuffer sb = new StringBuffer();
                        for (Map.Entry<String, Object> entry : details.entrySet()) {
                            try {
                                LinkedHashMap<String, Object> value = (LinkedHashMap<String, Object>) entry.getValue();
                                //服务状态
                                String serviceStatus = (String) value.get("status");
                                //如果不是成功状态
                                if (!"UP".equalsIgnoreCase(serviceStatus)) {
                                    //异常细节
                                    LinkedHashMap<String, Object> exceptionDetails = (LinkedHashMap<String, Object>) value.get("details");
                                    String error = (String) exceptionDetails.get("error");
                                    sb.append("节点:").append(entry.getKey()).append("<br>");
                                    sb.append("状态:").append(serviceStatus).append("<br>");
                                    sb.append(" 异常原因: ").append(error).append("<br>");
                                }
                            } catch (Exception e) {
                                //异常时,不应该抛出,而是继续打印异常
                                log.error("【获取-服务心跳异常邮件信息异常】", e);
                            }
                        }
                        //节点详细状态
                        model.put("details", sb.toString());
                        try {
                            //发送短信
                            sendEmailUtils.sendMail(model, instance_name.get("DOWN"),email_template, toMail, ccMail);
                            } catch (Exception e) {
                            log.error("【邮件发送超时...】", e);
                        }
                        break;
                    // 服务离线
                    case "OFFLINE":
                        log.error(instance.getRegistration().getServiceUrl() + " 发送 服务离线 的通知!");
                        try {
                            model.put("status", "服务下线");
                            model.put("message", ((InstanceStatusChangedEvent) event).getStatusInfo().getDetails().get("message"));
                            sendEmailUtils.sendMail(model, instance_name.get("OFFLINE"),email_template, toMail, ccMail,500);
                        } catch (Exception e) {
                            log.error("【邮件发送超时...】", e);
                        }
                        break;
                    //服务上线
                    case "UP":
                        log.info(instance.getRegistration().getServiceUrl() + "服务恢复");
                        //启动时间
                        String startup = instance.getRegistration().getMetadata().get("startup");
                        model.put("status", "服务恢复");
                        model.put("startup", startup);
                        try {
                            sendEmailUtils.sendMail(model, instance_name.get("UP"),email_template, toMail, ccMail);
                        } catch (Exception e) {
                            log.error("【邮件发送超时...】", e);
                        }
                        break;
                    // 服务未知异常
                    case "UNKNOWN":
                        log.error(instance.getRegistration().getServiceUrl() + "发送 服务未知异常 的通知!");
                        break;
                    default:
                        break;
                }

            } else {
                log.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(),
                        event.getType());
            }
        });
    }
}

自定义邮件模板:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>服务预警通知</title>
</head>
<body>

<p>服务状态:${status}</p>
<p>服务域名:${ipAddress}</p>
<p>服务名:${instanceName}</p>
<p>节点ID:${instanceId}</p>
<#if message??>
    异常原因:${message}
</#if>
<#if startup??>
    启动时间:${startup}
</#if>
<#if details??>
    <span style="font-weight:bold;">服务详细信息:</span><br>
    <span>${details}</span>
</#if>
</body>
</html>

1.2.4 自定义安全认证

因为monitor server端加入了security的安全控制,故依旧需要在配置文件或者代码中进行用户名,密码或者路径等的配置。

在客户端配置中,客户端在元数据中,将自己的用户名/密码传给了服务端,服务端可以进行参数的配置,以便可以访问到客户端的actuator端点。

@Configuration
public class monitorConfig {

    /**
     * springboot自动装配默认实现类,由于需要对配置密码进行解码操作,故手动装配
     * {@link AdminServerAutoConfiguration#basicAuthHttpHeadersProvider()}
     *
     * @return
     */
    @Bean
    public BasicAuthHttpHeaderProvider basicAuthHttpHeadersProvider() {
        return new BasicAuthHttpHeaderProvider() {
            @Override
            public HttpHeaders getHeaders(Instance instance) {
                HttpHeaders headers = new HttpHeaders();
                //获取用户名,密码
                String username = instance.getRegistration().getMetadata().get("user.name");
                String password = instance.getRegistration().getMetadata().get("user.password");
                String type = instance.getRegistration().getMetadata().get("user.type");

                //若是token有值,那么使用token认知
                if ("token".equalsIgnoreCase(type)) {
                    headers.set("X-Token",password);
                } else if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                    headers.set(HttpHeaders.AUTHORIZATION, encode(username, password));
                }
                return headers;
            }

            protected String encode(String username, String password) {
                String token = Base64Utils.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
                return "Basic " + token;
            }
        };
    }
}

1.2.5 自定义Http请求头

如果您需要将自定义HTTP标头注入到受监控应用程序的执行器端点的请求中,您可以轻松添加HttpHeadersProvider:

@Bean
public HttpHeadersProvider customHttpHeadersProvider() {
    return instance -> {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("X-CUSTOM", "My Custom Value");
        return httpHeaders;
    };
}

1.2.6 自定义拦截器

monitor Server向客户端发送请求时,会进入InstanceExchangeFilterFunction中,但是对于查询请求(即:actuator的端点请求),一般的请求方式是Get。我们可以在这里加入一些审计或者安全控制。

注:

@Bean
public InstanceExchangeFilterFunction auditLog() {
    return (instance, request, next) -> next.exchange(request).doOnSubscribe(s -> {
        if (HttpMethod.DELETE.equals(request.method()) || HttpMethod.POST.equals(request.method())) {
            log.info("{} for {} on {}", request.method(), instance.getId(), request.url());
        }
    });
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容