SpringBoot Admin升級boot等組件版本后無法監(jiān)控微服務(wù)問題
最近,安全中心報(bào)告了spring-boot-admin服務(wù)由于集成security版本過低導(dǎo)致了安全問題,Spring Security RegexRequestMatcher 認(rèn)證繞過漏洞(CVE-2022-22978),建議升級security版本,
- 5.5.x 版本使用者建議升級至5.5.7及其以上
- 5.6.x 版本使用者建議升級至5.6.4及其以上。
排查服務(wù)發(fā)現(xiàn)當(dāng)前服務(wù)集成security版本為5.3.9,于是決定動手升級。
一、升級版本
由于項(xiàng)目通過集成
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
接入security,因此security版本由springboot統(tǒng)一管理。
工程中各依賴版本如下:
<spring-boot.version>2.3.12.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
于是為了滿足上述升級需求,我們對該服務(wù)進(jìn)行版本調(diào)整。
<spring-boot.version>2.6.8</spring-boot.version> <spring-cloud.version>2021.0.1</spring-cloud.version> <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
springboot升級到2.4+之后,不再支持bootstrap版本,因此服務(wù)此時(shí)需要調(diào)整配置由bootstrap.yml轉(zhuǎn)變?yōu)閍pplication.yml。
spring:
application:
name: xxx
cloud:
nacos:
config:
username: xxx
password: xxx
server-addr: xxx:8848
namespace: xxx
group: DEFAULT_GROUP
refresh-enabled: false
config:
import:
- optional:nacos:xxx.properties?group=${spring.cloud.nacos.config.group}&refreshEnabled=false
- optional:nacos:xxx.properties?group=${spring.cloud.nacos.config.group}&refreshEnabled=false工程可以正常啟動,但訪問監(jiān)控頁面,監(jiān)控服務(wù)健康檢查處于DOWN狀態(tài),且無元數(shù)據(jù)。

二、排查原因
排查源碼發(fā)現(xiàn)spring-boot-admin-server-cloud源碼包中
InstanceDiscoveryListener通過監(jiān)聽event事件動態(tài)更新監(jiān)控服務(wù)狀態(tài)。
服務(wù)啟動后通過監(jiān)聽事件調(diào)用
InstanceDiscoveryListener.discover()方法。
protected void discover() {
log.debug("Discovering new instances from DiscoveryClient");
Flux.fromIterable(discoveryClient.getServices()).filter(this::shouldRegisterService)
.flatMapIterable(discoveryClient::getInstances).filter(this::shouldRegisterInstanceBasedOnMetadata)
.flatMap(this::registerInstance).collect(Collectors.toSet()).flatMap(this::removeStaleInstances)
.subscribe((v) -> {
}, (ex) -> log.error("Unexpected error.", ex));
}方法首先會從注冊中心獲取注冊服務(wù),之后屏蔽不需要監(jiān)控的服務(wù),再進(jìn)行bean轉(zhuǎn)換。
此處需要注意discoveryClient::getInstances方法,即bean轉(zhuǎn)換方法。
由于服務(wù)注冊中心為consul,因此getInstance會調(diào)用
ConsulDiscoveryClient.getInstances()。
public List<ServiceInstance> getInstances(final String serviceId, final QueryParams queryParams) {
List<ServiceInstance> instances = new ArrayList();
this.addInstancesToList(instances, serviceId, queryParams);
return instances;
}
private void addInstancesToList(List<ServiceInstance> instances, String serviceId, QueryParams queryParams) {
Builder requestBuilder = HealthServicesRequest.newBuilder().setPassing(this.properties.isQueryPassing()).setQueryParams(queryParams).setToken(this.properties.getAclToken());
String[] queryTags = this.properties.getQueryTagsForService(serviceId);
if (queryTags != null) {
requestBuilder.setTags(queryTags);
}
HealthServicesRequest request = requestBuilder.build();
Response<List<HealthService>> services = this.client.getHealthServices(serviceId, request);
Iterator var8 = ((List)services.getValue()).iterator();
while(var8.hasNext()) {
HealthService service = (HealthService)var8.next();
instances.add(new ConsulServiceInstance(service, serviceId));
}
}方法中真正的bean轉(zhuǎn)換在于new ConsulServiceInstance(service, serviceId)。
該構(gòu)造器方法如下:
public ConsulServiceInstance(HealthService healthService, String serviceId) {
this(healthService.getService().getId(), serviceId, ConsulServerUtils.findHost(healthService), healthService.getService().getPort(), getSecure(healthService), getMetadata(healthService), healthService.getService().getTags());
this.healthService = healthService;
}可見metadata數(shù)據(jù)通過getMetadata()方法獲取。
private static Map<String, String> getMetadata(HealthService healthService) {
Map<String, String> metadata = healthService.getService().getMeta();
if (metadata == null) {
metadata = new LinkedHashMap();
}
return (Map)metadata;
}從代碼看出,metadata數(shù)據(jù)從service實(shí)例的meta屬性中獲取。而由于監(jiān)控服務(wù)統(tǒng)一注冊tag標(biāo)簽而非meta到注冊中心,因此此處數(shù)據(jù)獲取為空,從而導(dǎo)致spring-boot-admin 元數(shù)據(jù)及監(jiān)控路徑拼接異常,進(jìn)而服務(wù)監(jiān)控失敗,狀態(tài)DOWN。
那么為什么之前可以正常監(jiān)控,升級版本之后便監(jiān)控異常呢?
通過回退之前版本我們發(fā)現(xiàn),老版本中meta數(shù)據(jù)獲取方法并非當(dāng)前這樣。
private void addInstancesToList(List<ServiceInstance> instances, String serviceId, QueryParams queryParams) {
Builder requestBuilder = HealthServicesRequest.newBuilder().setPassing(this.properties.isQueryPassing()).setQueryParams(queryParams).setToken(this.properties.getAclToken());
String queryTag = this.properties.getQueryTagForService(serviceId);
if (queryTag != null) {
requestBuilder.setTag(queryTag);
}
HealthServicesRequest request = requestBuilder.build();
Response<List<HealthService>> services = this.client.getHealthServices(serviceId, request);
HealthService service;
String host;
Map metadata;
boolean secure;
for(Iterator var8 = ((List)services.getValue()).iterator(); var8.hasNext(); instances.add(new DefaultServiceInstance(service.getService().getId(), serviceId, host, service.getService().getPort(), secure, metadata))) {
service = (HealthService)var8.next();
host = ConsulServerUtils.findHost(service);
metadata = ConsulServerUtils.getMetadata(service, this.properties.isTagsAsMetadata());
secure = false;
if (metadata != null && metadata.containsKey("secure")) {
secure = Boolean.parseBoolean((String)metadata.get("secure"));
}
}
}老版本通過調(diào)用ConsulServerUtils.getMetadata(service, this.properties.isTagsAsMetadata());獲取meta數(shù)據(jù)。
@Deprecated
public static Map<String, String> getMetadata(HealthService healthService, boolean tagsAsMetadata) {
return tagsAsMetadata ? getMetadata(healthService.getService().getTags()) : healthService.getService().getMeta();
}此版本中metadata通過tagsAsMetadata 屬性標(biāo)識判斷是否可從tags中獲取metadata,由于該屬性默認(rèn)為true,因此我們之前注冊到tags的屬性可以被admin標(biāo)記為metadata,從而參與到
DefaultServiceInstanceConverter.convert()實(shí)現(xiàn)對Registration對象的組裝。
protected Mono<InstanceId> registerInstance(ServiceInstance instance) {
try {
Registration registration = this.converter.convert(instance).toBuilder().source("discovery").build();
log.debug("Registering discovered instance {}", registration);
return this.registry.register(registration);
} catch (Exception var3) {
log.error("Couldn't register instance for discovered instance ({})", this.toString(instance), var3);
return Mono.empty();
}
}此時(shí),我們只需要讓監(jiān)控服務(wù)注冊meta到consul注冊中心即可。
根據(jù)spring-cloud-consul官方文檔Spring Cloud Consul, metadata使用實(shí)例如下(也可使用properties文件,自行轉(zhuǎn)換即可)

此處有特別說明:

參考DefaultServiceInstanceConverter所定義的key屬性,于是我們修改配置文件,再次重新啟動微服務(wù)。
此時(shí),服務(wù)再次報(bào)錯(cuò)。
OperationException{statusCode=400, statusMessage='Bad Request', statusContent='Invalid Service Meta: Couldn't load metadata pair ('management.context-path', '/xxx/actuator'): Key contains invalid characters'}
根據(jù)異常棧,我們服務(wù)該異常由調(diào)用consul服務(wù)端rpc之后response響應(yīng)體返回。

由于是遠(yuǎn)端服務(wù)返回,考慮版本兼容性問題,直接升級最高版本consul,升級之后發(fā)現(xiàn)異常仍然存在。
為了驗(yàn)證該問題,我們切換注冊中心到nacos,發(fā)現(xiàn)meta注冊正常,監(jiān)控正常。
到此,該問題排查基本結(jié)束,定義為spring-boot-admin與spring-cloud-consul版本兼容性問題。
spring-cloud-consul在v3.0.0(不包含M1等版本)及之上,meta數(shù)據(jù)獲取不再兼容tags,因此使用該版本的cloud在于admin集成時(shí),若監(jiān)控服務(wù)存在context-path或者自定義的健康檢查路徑等,由于admin定義meata屬性key均包含"."字符,因此大概率均會出現(xiàn)監(jiān)控異常問題(推測結(jié)論,未完整驗(yàn)證所有版本)。
目前,已提issue到github上spring-boot-admin中,參見https://github.com/codecentric/spring-boot-admin/issues/2076,等待官方解答中。
***************************************
該問題目前已解決,根據(jù)版本匹配關(guān)系,spring-boot需升級到2.7.0版本才可,而非2.6.8。
spring-boot-admin在2.7.4版本對metadatda做了兼容處理。

且官方文檔也指出

同時(shí),據(jù)回復(fù),consul端也在推進(jìn)處理此問題

總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springmvc和js前端的數(shù)據(jù)傳遞和接收方式(兩種)
本文介紹了springmvc和js前端的數(shù)據(jù)傳遞和接收方式(兩種),詳細(xì)的介紹了兩種方式,一種是json格式傳遞,另一種是Map傳遞,具有一定的參考價(jià)值,有興趣的可以了解一下2017-12-12
SpringBoot監(jiān)控Tomcat活動線程數(shù)來判斷是否完成請求處理方式
這篇文章主要介紹了SpringBoot監(jiān)控Tomcat活動線程數(shù)來判斷是否完成請求處理方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
SpringBoot中讀取jar包中的resources目錄下的文件的三種方式
這篇文章給大家總結(jié)了SpringBoot讀取 jar 包中的 resources 目錄下的文件的三種方式,文中有詳細(xì)的代碼示例供大家參考,,需要的朋友可以參考下2023-06-06
spring聲明式事務(wù)@Transactional開發(fā)常犯的幾個(gè)錯(cuò)誤及最新解決方案
使用聲明式事務(wù)@Transactional進(jìn)行事務(wù)一致性的管理,在開發(fā)過程中,發(fā)現(xiàn)很多開發(fā)同學(xué)都用錯(cuò)了spring聲明式事務(wù)@Transactional或使用不規(guī)范,導(dǎo)致出現(xiàn)各種事務(wù)問題,這篇文章主要介紹了spring聲明式事務(wù)@Transactional開發(fā)常犯的幾個(gè)錯(cuò)誤及解決辦法,需要的朋友可以參考下2024-02-02
Java中BigDecimal,DateFormatter?和迭代器的"陷阱"
這篇文章主要介紹了Java中BigDecimal,DateFormatter?和迭代器的"陷阱",文章圍繞主題展開詳細(xì)的內(nèi)容介紹,感興趣的小伙伴可以參考一下2022-06-06

