JAVA && Spring && SpringBoot2.x — 學(xué)習(xí)目錄
SpringBoot Admin原理就是使用SpringBoot actuator提供的端點(diǎn)熊尉,可以通過HTTP訪問蛛芥。將得到的信息顯示在頁面上惜姐。需要注意的是:SpringBoot Actuator端點(diǎn)顯示的是整個(gè)服務(wù)生命周期中的相關(guān)信息排宰,若是應(yīng)用部署在公網(wǎng)上扔枫,要慎重選擇公開的端點(diǎn)蛹磺。為了端點(diǎn)的安全性搓劫,可以引入Spring Security進(jìn)行權(quán)限控制。
1. 相關(guān)配置
1.1 [客戶端]相關(guān)配置
1.1.1 pom文件
<!--監(jiān)控-客戶端-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.1.6</version>
</dependency>
<!--權(quán)限控制-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.1.2 配置文件
spring:
boot:
admin:
client:
url: http://localhost:7000 #監(jiān)控-服務(wù)器地址
instance:
# service-base-url: http://127.0.0.1:8080 #自定義節(jié)點(diǎn)的ip地址
prefer-ip: true #是否顯示真實(shí)的ip地址
#元數(shù)據(jù)混巧,用于配置monitor server訪問client端的憑證
metadata:
user.name: user
user.password: 123456
#client可以連接到monitor server端的憑證
username: admin
password: 123456
read-timeout: 10000 #讀取超時(shí)時(shí)間
application:
#應(yīng)用名
name: 監(jiān)控 客戶端測試項(xiàng)目
#公開所有的端點(diǎn)
management:
endpoints:
web:
exposure:
#展示某些端點(diǎn)(默認(rèn)展示health,info枪向,其余均禁止)
include: health,info,metrics
# CORS跨域支持
cors:
allowed-origins: "*"
allowed-methods: GET,POST
#health端點(diǎn)的訪問權(quán)限
endpoint:
health:
#選擇展示
show-details: always
health:
mail:
enabled: false #不監(jiān)控郵件服務(wù)器狀態(tài)
#自定義的健康信息,使用@Message@取得的是maven中的配置信息
info:
version: @project.version@
groupId: @project.groupId@
artifactId: @project.artifactId@
#顯示所有的健康信息
1.1.3 安全控制
因?yàn)榭蛻舳诵枰┞兑恍┒它c(diǎn)(SpringBoot Actuator)咧党,若是服務(wù)部署在外網(wǎng)上秘蛔,可能會(huì)造成信息泄露,故需要使用Spring Security進(jìn)行安全認(rèn)證傍衡。
需要注意的是:若是加入了security的maven依賴后深员,會(huì)自動(dòng)的對(duì)所有路徑使用httpBasic安全認(rèn)證。
配置認(rèn)證規(guī)則和加密模式:
@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 自定義授權(quán)規(guī)則蛙埂,只對(duì)端點(diǎn)進(jìn)行安全訪問
*
* @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需要認(rèn)證
// .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)]
后倦畅,無需在配置文件中進(jìn)行Security的用戶名,密碼配置箱残。即:
spring.boot.admin.client:
username: user
password: 123456
1.1.4 如何動(dòng)態(tài)的配置參數(shù)
監(jiān)控客戶端滔迈,需要在配置文件中填寫監(jiān)控服務(wù)端的安全憑證以及客戶端的安全憑證,但是將[用戶名被辑,密碼]明文的配置在配置文件中燎悍,可能會(huì)造成一些安全隱患。那么如何在代碼中動(dòng)態(tài)的進(jìn)行參數(shù)的配置呢盼理?
@Configuration
public class AdminClientConfig {
/**
* 配置文件谈山,修改SpringBoot的自動(dòng)裝配
*
* {@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原數(shù)據(jù),進(jìn)行個(gè)性化的業(yè)務(wù)操作宏怔,例如在數(shù)據(jù)庫中動(dòng)態(tài)獲茸嗦贰(密碼)
String username = instance.getMetadata().get("user.name");
return new ServletApplicationFactory(instance,
management,
server,
servletContext,
pathMappedEndpoints,
webEndpoint,
metadataContributor,
dispatcherServletPath
);
}
}
/**
* 注冊(cè)的程序
* {@link SpringBootAdminClientAutoConfiguration#registrator(ClientProperties, ApplicationFactory)}
* @param client
* @param applicationFactory
* @return
*/
@Bean
public ApplicationRegistrator registrator(ClientProperties client, ApplicationFactory applicationFactory) {
//設(shè)置RestTemplateBuilder參數(shù)
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 [服務(wù)端]相關(guān)配置
1.2.1 pom配置
<!--監(jiān)控服務(wù)端-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.1.6</version>
</dependency>
<!--整合安全機(jī)制-->
<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監(jiān)控配置start
application:
name: spring-boot-admin
boot:
admin:
notify:
mail:
enabled: false #關(guān)閉admin自帶的郵件通知
monitor:
read-timeout: 200000
ui:
title: 服務(wù)監(jiān)控
mail:
host: smtp.example.com
username: admin@example.com #郵箱地址
password: xxxxxxxxxx #授權(quán)碼
properties:
mail:
smtp:
starttls:
enable: true
required: true
freemarker:
settings:
classic_compatible: true #解決模板空指針問題
#springboot adminUi監(jiān)控配置end
需要注意的是:若是采用SpringBoot Admin自帶的郵件通知,那么不能按照業(yè)務(wù)進(jìn)行分組通知臊诊,需要我們關(guān)閉自帶的郵件通知鸽粉,手動(dòng)進(jìn)行通知。
1.2.3 自定義通知
您可以通過添加實(shí)現(xiàn)Notifier接口的Spring Beans來添加您自己的通知程序抓艳,最好通過擴(kuò)展 AbstractEventNotifier或AbstractStatusChangeNotifier触机。
可參考源碼自定義通知de.codecentric.boot.admin.server.notify.MailNotifier
@Component
@Slf4j
public class CustomMailNotifier extends AbstractStatusChangeNotifier {
//自定義郵件發(fā)送類
@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","服務(wù)心跳異常通知");
instance_name.put("OFFLINE","服務(wù)下線報(bào)警通知");
instance_name.put("UP","服務(wù)恢復(fù)通知");
instance_name.put("UNKNOWN","服務(wù)未知異常");
}
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);
//獲取服務(wù)地址
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);
//郵件接收者,可根據(jù)instanceName靈活配置
String toMail = "xxx@qq.com";
String[] ccMail = {"xxxx@qq.com"玷或,"yyy@qq.com"};
switch (status) {
// 健康檢查沒通過
case "DOWN":
log.error(instance.getRegistration().getServiceUrl() + "服務(wù)心跳異常儡首。");
model.put("status", "服務(wù)心跳異常");
Map<String, Object> details = instance.getStatusInfo().getDetails();
//遍歷Map,查找down掉的服務(wù)
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();
//服務(wù)狀態(tài)
String serviceStatus = (String) value.get("status");
//如果不是成功狀態(tài)
if (!"UP".equalsIgnoreCase(serviceStatus)) {
//異常細(xì)節(jié)
LinkedHashMap<String, Object> exceptionDetails = (LinkedHashMap<String, Object>) value.get("details");
String error = (String) exceptionDetails.get("error");
sb.append("節(jié)點(diǎn):").append(entry.getKey()).append("<br>");
sb.append("狀態(tài):").append(serviceStatus).append("<br>");
sb.append(" 異常原因: ").append(error).append("<br>");
}
} catch (Exception e) {
//異常時(shí)偏友,不應(yīng)該拋出蔬胯,而是繼續(xù)打印異常
log.error("【獲取-服務(wù)心跳異常郵件信息異常】", e);
}
}
//節(jié)點(diǎn)詳細(xì)狀態(tài)
model.put("details", sb.toString());
try {
//發(fā)送短信
sendEmailUtils.sendMail(model, instance_name.get("DOWN"),email_template, toMail, ccMail);
} catch (Exception e) {
log.error("【郵件發(fā)送超時(shí)...】", e);
}
break;
// 服務(wù)離線
case "OFFLINE":
log.error(instance.getRegistration().getServiceUrl() + " 發(fā)送 服務(wù)離線 的通知位他!");
try {
model.put("status", "服務(wù)下線");
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("【郵件發(fā)送超時(shí)...】", e);
}
break;
//服務(wù)上線
case "UP":
log.info(instance.getRegistration().getServiceUrl() + "服務(wù)恢復(fù)");
//啟動(dòng)時(shí)間
String startup = instance.getRegistration().getMetadata().get("startup");
model.put("status", "服務(wù)恢復(fù)");
model.put("startup", startup);
try {
sendEmailUtils.sendMail(model, instance_name.get("UP"),email_template, toMail, ccMail);
} catch (Exception e) {
log.error("【郵件發(fā)送超時(shí)...】", e);
}
break;
// 服務(wù)未知異常
case "UNKNOWN":
log.error(instance.getRegistration().getServiceUrl() + "發(fā)送 服務(wù)未知異常 的通知氛濒!");
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>服務(wù)預(yù)警通知</title>
</head>
<body>
<p>服務(wù)狀態(tài):${status}</p>
<p>服務(wù)域名:${ipAddress}</p>
<p>服務(wù)名:${instanceName}</p>
<p>節(jié)點(diǎn)ID:${instanceId}</p>
<#if message??>
異常原因:${message}
</#if>
<#if startup??>
啟動(dòng)時(shí)間:${startup}
</#if>
<#if details??>
<span style="font-weight:bold;">服務(wù)詳細(xì)信息:</span><br>
<span>${details}</span>
</#if>
</body>
</html>
1.2.4 自定義安全認(rèn)證
因?yàn)閙onitor server端加入了security的安全控制产场,故依舊需要在配置文件或者代碼中進(jìn)行用戶名,密碼或者路徑等的配置泼橘。
在客戶端配置中涝动,客戶端在元數(shù)據(jù)中,將自己的用戶名/密碼傳給了服務(wù)端炬灭,服務(wù)端可以進(jìn)行參數(shù)的配置醋粟,以便可以訪問到客戶端的actuator端點(diǎn)。
@Configuration
public class monitorConfig {
/**
* springboot自動(dòng)裝配默認(rèn)實(shí)現(xiàn)類重归,由于需要對(duì)配置密碼進(jìn)行解碼操作米愿,故手動(dòng)裝配
* {@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認(rèn)知
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請(qǐng)求頭
如果您需要將自定義HTTP標(biāo)頭注入到受監(jiān)控應(yīng)用程序的執(zhí)行器端點(diǎn)的請(qǐng)求中育苟,您可以輕松添加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向客戶端發(fā)送請(qǐng)求時(shí),會(huì)進(jìn)入
InstanceExchangeFilterFunction
中椎木,但是對(duì)于查詢請(qǐng)求(即:actuator的端點(diǎn)請(qǐng)求)违柏,一般的請(qǐng)求方式是Get。我們可以在這里加入一些審計(jì)或者安全控制香椎。
注:
@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());
}
});
}