最近,安全中心报告了spring-boot-admin服务由于集成security版本过低导致了安全问题,Spring Security RegexRequestMatcher 认证绕过漏洞(CVE-2022-22978),建议升级security版本,
5.5.x 版本使用者建议升级至5.5.7及其以上
5.6.x 版本使用者建议升级至5.6.4及其以上。
排查服务发现当前服务集成security版本为5.3.9,于是决定动手升级。
一、升级版本
由于项目通过集成
org.springframework.boot spring-boot-starter-security
接入security,因此security版本由springboot统一管理。
工程中各依赖版本如下:
- <spring-boot.version>2.3.12.RELEASEspring-boot.version>
- <spring-cloud.version>Hoxton.SR12spring-cloud.version>
- <spring-cloud-alibaba.version>2.2.6.RELEASEspring-cloud-alibaba.version>
于是为了满足上述升级需求,我们对该服务进行版本调整。
- <spring-boot.version>2.6.8spring-boot.version>
- <spring-cloud.version>2021.0.1spring-cloud.version>
- <spring-cloud-alibaba.version>2021.0.1.0spring-cloud-alibaba.version>
springboot升级到2.4+之后,不再支持bootstrap版本,因此服务此时需要调整配置由bootstrap.yml转变为application.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
工程可以正常启动,但访问监控页面,监控服务健康检查处于DOWN状态,且无元数据。

二、排查原因
排查源码发现spring-boot-admin-server-cloud源码包中
InstanceDiscoveryListener通过监听event事件动态更新监控服务状态。
服务启动后通过监听事件调用
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));
- }
方法首先会从注册中心获取注册服务,之后屏蔽不需要监控的服务,再进行bean转换。
此处需要注意discoveryClient::getInstances方法,即bean转换方法。
由于服务注册中心为consul,因此getInstance会调用
ConsulDiscoveryClient.getInstances()。
- public List
getInstances(final String serviceId, final QueryParams queryParams) { - List
instances = new ArrayList(); - this.addInstancesToList(instances, serviceId, queryParams);
- return instances;
- }
-
- private void addInstancesToList(List
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
> 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转换在于new ConsulServiceInstance(service, serviceId)。
该构造器方法如下:
- 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数据通过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数据从service实例的meta属性中获取。而由于监控服务统一注册tag标签而非meta到注册中心,因此此处数据获取为空,从而导致spring-boot-admin 元数据及监控路径拼接异常,进而服务监控失败,状态DOWN。
那么为什么之前可以正常监控,升级版本之后便监控异常呢?
通过回退之前版本我们发现,老版本中meta数据获取方法并非当前这样。
- private void addInstancesToList(List
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"));
- }
- }
-
- }
老版本通过调用ConsulServerUtils.getMetadata(service, this.properties.isTagsAsMetadata());获取meta数据。
- @Deprecated
- public static Map<String, String> getMetadata(HealthService healthService, boolean tagsAsMetadata) {
- return tagsAsMetadata ? getMetadata(healthService.getService().getTags()) : healthService.getService().getMeta();
- }
此版本中metadata通过tagsAsMetadata 属性标识判断是否可从tags中获取metadata,由于该属性默认为true,因此我们之前注册到tags的属性可以被admin标记为metadata,从而参与到
DefaultServiceInstanceConverter.convert()实现对Registration对象的组装。
- protected Mono
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();
- }
- }
此时,我们只需要让监控服务注册meta到consul注册中心即可。
根据spring-cloud-consul官方文档Spring Cloud Consul, metadata使用实例如下(也可使用properties文件,自行转换即可)

此处有特别说明:

参考DefaultServiceInstanceConverter所定义的key属性,于是我们修改配置文件,再次重新启动微服务。
此时,服务再次报错。
OperationException{statusCode=400, statusMessage='Bad Request', statusContent='Invalid Service Meta: Couldn't load metadata pair ('management.context-path', '/xxx/actuator'): Key contains invalid characters'}
根据异常栈,我们服务该异常由调用consul服务端rpc之后response响应体返回。

由于是远端服务返回,考虑版本兼容性问题,直接升级最高版本consul,升级之后发现异常仍然存在。
为了验证该问题,我们切换注册中心到nacos,发现meta注册正常,监控正常。
到此,该问题排查基本结束,定义为spring-boot-admin与spring-cloud-consul版本兼容性问题。
spring-cloud-consul在v3.0.0(不包含M1等版本)及之上,meta数据获取不再兼容tags,因此使用该版本的cloud在于admin集成时,若监控服务存在context-path或者自定义的健康检查路径等,由于admin定义meata属性key均包含"."字符,因此大概率均会出现监控异常问题(推测结论,未完整验证所有版本)。
目前,已提issue到github上spring-boot-admin中,参见https://github.com/codecentric/spring-boot-admin/issues/2076,等待官方解答中。