SpringBoot Admin升級boot等組件版本后無法監(jiān)控微服務(wù)問題
最近,安全中心報告了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,于是決定動手升級。
一、升級版本
由于項目通過集成
<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ù)此時需要調(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實例的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()實現(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(); } }
此時,我們只需要讓監(jiān)控服務(wù)注冊meta到consul注冊中心即可。
根據(jù)spring-cloud-consul官方文檔Spring Cloud Consul, metadata使用實例如下(也可使用properties文件,自行轉(zhuǎn)換即可)
此處有特別說明:
參考DefaultServiceInstanceConverter所定義的key屬性,于是我們修改配置文件,再次重新啟動微服務(wù)。
此時,服務(wù)再次報錯。
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)異常仍然存在。
為了驗證該問題,我們切換注冊中心到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集成時,若監(jiān)控服務(wù)存在context-path或者自定義的健康檢查路徑等,由于admin定義meata屬性key均包含"."字符,因此大概率均會出現(xiàn)監(jiān)控異常問題(推測結(jié)論,未完整驗證所有版本)。
目前,已提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做了兼容處理。
且官方文檔也指出
同時,據(jù)回復(fù),consul端也在推進(jìn)處理此問題
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java如何獲取request中json數(shù)據(jù)
這篇文章主要給大家介紹了關(guān)于java如何獲取request中json數(shù)據(jù)的相關(guān)資料,文中通過代碼示例以及圖文將獲取的方法介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用java具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08Spring boot整合Mybatis實現(xiàn)級聯(lián)一對多CRUD操作的完整步驟
這篇文章主要給大家介紹了關(guān)于Spring boot整合Mybatis實現(xiàn)級聯(lián)一對多CRUD操作的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解
這篇文章主要介紹了SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09Spring整合Quartz分布式調(diào)度的示例代碼
本篇文章主要介紹了Spring整合Quartz分布式調(diào)度的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04