有些朋友可能是在上线前一天才发现/actuator/env裸奔在公网上,有些是等监控告警炸了才反应过来。无论哪种,Spring Boot Actuator 不认证明晃晃地挂在生产环境,就跟把服务器 root 密码写在便签纸上贴在机箱上一样——看着没事,出事就是大事。这篇文章直接讲清楚 Actuator 为什么要加认证、怎么加最稳、以及我在实际排查中踩过哪些坑。适合用过 Spring Boot 但没认真处理过 Actuator 安全的人,也适合正在紧急补救线上问题、需要照着操作就生效的运维和开发同学。
1. 先搞清楚:Actuator 暴露了哪些"刀"
1.1 Actuator 到底是什么
Actuator 是 Spring Boot 自带的一套运行时监控与运维端点。它能在应用运行过程中向外提供健康状态、配置信息、线程快照、堆内存转储、日志级别调整等能力。对运维来说,它是排查问题的利器;但对攻击者来说,它就是一张标注好"从哪里入手"的地图。
Spring Boot 2.x 之后,Actuator 的端点路径统一挂在/actuator下面,比如/actuator/health、/actuator/env、/actuator/mappings。你可以通过management.endpoints.web.exposure.include配置控制哪些端点对外暴露。很多人为了图省事,直接写成include: "*",等于把家底全亮出来了。
1.2 不认证的风险到底有多大
先说结论:不认证的 Actuator,危险程度不亚于把数据库公网端口打开。
几个典型的敏感端点拆开看:
/actuator/env:返回全部环境配置属性。说白了,所有application.yml里写的、JVM 启动参数里带的、环境变量里注入的,都可能在这里以明文方式吐出来。数据库口令、Redis 密码、各种密钥全部裸奔。我在一次排查里见过某团队把云厂商的 SecretKey 直接写在配置里,/actuator/env一访问,整个基础设施的权限等于拱手送人。/actuator/heapdump:导出 JVM 堆内存快照。堆里有连接池中的数据库连接串、认证后的 Token、业务数据的缓存对象。拿到 heapdump 文件后离线分析,能把内存里的明文密码全部翻出来。这不是理论风险,而是黑产拿分最快的路径之一。/actuator/threaddump:导出所有线程栈。攻击者可以从中分析业务调用链路、内部类名、接口路径,为下一步定向攻击做情报收集。/actuator/mappings:列出当前应用所有 URL 映射关系。相当于把攻击面清单直接递到对方手里。/actuator/shutdown:如果被打开且没有认证,直接关停应用。虽然默认是关闭的,但总有手滑的人把它打开了。
1.3 默认暴露策略和常见的裸奔姿势
Spring Boot 2.x 之后,默认只暴露health一个端点,本意是安全的。但实际项目里裸奔通常是下面几种原因:
- 为了调试方便,在配置里写
include: "*",上线前忘记收拢。 - 配合 Spring Boot Admin 做监控,为了省事把所有端点都开放了。
- 以为内网部署就安全,结果集群节点被扫到公网端口,导致完全暴露。
- 引入了
spring-boot-starter-actuator,但完全不知道它有对外 HTTP 端口,稀里糊涂就暴露了。
health端点本身不算敏感,但要注意show-details参数。如果设成always,健康检查详情里会暴露数据库连接状态、磁盘空间、Redis 连接池等信息,等于把内部架构图拍给外人看。
2. 修复方案选型:为什么我推荐 Spring Security 这条路
2.1 主流方案的横向对比
给 Actuator 加认证,业界大致有几条路线:
| 方案 | 实现成本 | 覆盖范围 | 维护难度 | 适用场景 |
|---|---|---|---|---|
| Spring Security + HttpBasic | 低,依赖一个 starter | 可以精确控制每一个/actuator/**端点 | 低 | 绝大多数 Spring Boot 应用 |
| 自定义 Filter / Interceptor 做 Token 校验 | 中,需要自己写逻辑 | 只覆盖你自己写的过滤逻辑 | 中 | 不想引入 Spring Security、只做轻量防护 |
management.server.port独立端口 + 网络安全组 | 低,配置即可 | 通过防火墙规则控制来源 IP | 低 | 内网部署、有网络策略权限的团队 |
| 网关层统一鉴权 | 中,取决于网关体系 | 只在网关层生效,绕过网关就失效 | 中 | 有统一流量入口的微服务架构 |
先说结论:如果你只有一个 Spring Boot 应用,或者多个应用都在同一个技术栈里,直接用 Spring Security 是最稳的方案。它不需要额外部署网关,不需要网络层面协调,一个依赖加一段配置就能覆盖全部 Actuator 端点。
2.2 三个决定性因素
我选方案的时候主要看三个方面:
第一是团队能不能维护住。自定义 Filter 看起来很轻量,但要考虑异常处理、Token 过期、加密存储、日志记录。这些东西自己写一遍,维护几个月后就会觉得还是用现成框架香。
第二是会不会影响现有业务接口。Spring Security 有个"坑":一旦引入,默认会拦截所有请求。如果你不想让业务登录逻辑跟着变复杂,就得在配置里写清楚"只保护/actuator/**,其他放开"。这一点我在第 3 部分会给出可以直接抄的配置。
第三是网络层面能不能兜底。就算加了认证,我依然建议同时把 Actuator 放到一个独立端口或者独立的内网网段里。不是多此一举,而是认证只解决"谁能访问",网络隔离解决"谁能到达",两层叠加才是生产级的安全姿态。
2.3 一种看起来省事但我不推荐的姿势
有些文章会建议"不用引入 Spring Security,自己写个 OncePerRequestFilter 判断 Header 里的 Token 就行"。我实际用过这个方案,在小项目里确实能跑,但有三个隐患:
- Token 硬编码在代码或配置文件里,换一次要发一次版本。
- 异常处理不完善的话,认证失败可能返回 500 而不是 401,调用方很难排查。
- 只挡了 HTTP 入口,如果以后走了消息队列或定时任务调用 Actuator 的内部方法,过滤链根本不会经过。
如果你确实不想用 Spring Security,至少也得用独立端口 + 网络白名单的组合。纯粹靠 Header Token 扛生产流量,算是给自己埋雷。
3. 实操:给 Actuator 加上最稳的认证
3.1 引入依赖与基础配置
Spring Boot 场景下,加认证最直接的方式是引入 Spring Security 依赖。以 Maven 为例:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>如果是 Gradle:
implementation 'org.springframework.boot:spring-boot-starter-security'引入依赖之后,先别急着写代码,第一件事是去把 Actuator 的暴露范围收敛一下。不要用*,按需列出真正要用的端点:
management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when-authorized这段配置的思路是:只暴露健康检查、基础信息、监控指标和 Prometheus 抓取端点。真正的敏感端点,比如env、configprops、heapdump、threaddump、mappings,直接不在暴露范围内。这是第一道闸门。
show-details: when-authorized的意义在于,匿名访问/actuator/health只能看到{"status":"UP"},而带着认证信息访问时才能看到磁盘空间、数据库连接等细节。既满足了负载均衡器和运维脚本的健康检查需求,又不会泄露内部架构。
3.2 配置 Spring Security 认证规则
引入 Spring Security 后,不加任何配置,默认行为是:所有请求都要认证。很多人加完依赖重启,发现业务接口全 401 了,就是这个原因。
正确做法是写一个SecurityFilterChain配置,只保护/actuator/**,其他请求放行。以 Spring Boot 3.x 为例:
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class ActuatorSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .requestMatchers("/actuator/info").permitAll() .requestMatchers("/actuator/**").authenticated() .anyRequest().permitAll() ) .httpBasic(httpBasic -> {}); return http.build(); } }这段配置要做的事情一件一件拆开看:
requestMatchers("/actuator/health").permitAll():健康检查端点放行,让负载均衡器、容器编排平台能匿名探测存活状态。requestMatchers("/actuator/info").permitAll():应用信息一般不含敏感数据,也可以放行;如果你觉得内容可能包含版本细节或环境标识,那就把它也放进认证范围。requestMatchers("/actuator/**").authenticated():所有其他 Actuator 端点必须登录。anyRequest().permitAll():业务接口全部放行,避免影响正常登录之外的开放接口。httpBasic(httpBasic -> {}):启用 HTTP Basic 认证。之所以用 Basic 而不是表单登录,是因为运维脚本、监控系统、curl 命令都天然支持 Basic,不需要处理 Session 和 Cookie。
如果你用的是 Spring Boot 2.x,写法只有一点不同:requestMatchers版本在 2.4 之后也支持,但如果你还停留在 2.3 或更早版本,要换成antMatchers。我的建议是尽量升到 2.4 以上,统一用requestMatchers,免得以后升级又踩一遍。
3.3 设置用户与密码
认证规则配好了,还要告诉 Spring Security"谁有资格访问"。最简单的是在配置文件里定义内存用户:
spring: security: user: name: monitor password: ${ACTUATOR_PASSWORD} roles: ACTUATOR注意这里我把密码写成了${ACTUATOR_PASSWORD},也就是从环境变量读取。千万别直接在 yml 里写死明文密码,否则你保护了半天,密码自己也跟着应用源码一起泄露了。
生产环境可以用这样的方式注入:
export ACTUATOR_PASSWORD=$(openssl rand -base64 32)然后把环境变量交给部署平台保管,比如 Kubernetes 的 Secret、云厂商的配置中心。启动后,访问受保护端点就要带用户名和密码:
curl -u monitor:你的密码 http://localhost:8080/actuator/metrics如果你不想定义roles,也可以不用,直接authenticated()只判断"是否登录",不区分角色。但如果后续有不同团队需要不同权限,就可以引入@PreAuthorize或hasRole('ACTUATOR')做更细的控制。初学者不推荐一开始就把角色体系做复杂,等真有需求再加也不迟。
3.4 验证修复是否生效
配置完成后,怎么确认真的稳了?我习惯三步验证:
第一步,匿名访问敏感端点,应该返回 401:
curl -i http://localhost:8080/actuator/env预期响应头里有HTTP/1.1 401,说明匿名访问被挡住了。如果返回 200,说明认证规则没生效,回去检查@EnableWebSecurity和过滤器链顺序。
第二步,带凭据访问,应该能拿到数据:
curl -u monitor:你的密码 http://localhost:8080/actuator/metrics预期返回 JSON 格式的指标列表。
第三步,检查健康检查端点仍然匿名可访问:
curl http://localhost:8080/actuator/health预期返回{"status":"UP"}。如果这个时候让你输入密码,说明permitAll()的放行逻辑写错了,或者安全过滤器的匹配顺序有问题。
另外要提醒:有些网关或代理层会做一层 Basic Auth 缓存。你改了密码后,网关层可能还拿着旧凭据去探活,导致看起来"认证失效"或者"认证没加成功"。验证的时候最好绕过网关直接打到应用端口。
4. 生产环境踩坑实录与排查表
4.1 场景一:引入 Spring Security 后,业务接口全部 401
这是加依赖之后最常见的翻车现场。原因很简单:Spring Security 一旦在 classpath 里,默认就会把所有接口保护起来。如果你只写了requestMatchers("/actuator/**").authenticated(),忘了anyRequest().permitAll(),那业务接口就会全部要求登录。
我当时帮某团队排查,他们升级了一个安全补丁,结果全站接口 401,连登录接口都进不去。追到最后就是过滤器链里没给anyRequest()放行。加上这一行之后,业务恢复,只保护 Actuator。
4.2 场景二:负载均衡器健康检查挂了
很多负载均衡器会定时访问/actuator/health来探活。如果你把所有 Actuator 端点都圈进了认证范围,探活请求就会收到 401,然后被判定为不健康。
解决办法有两个:
- 把
/actuator/health单独permitAll(),同时把show-details设置成when-authorized,这样匿名只能看到状态,带凭据能看到详情。 - 或者让健康检查请求也带上凭据。
第一种更干净。你不想让外部健康检查携带凭据,更不想让匿名访问者看到连接池详情,两个诉求靠permitAll()加show-details就同时满足了。
4.3 场景三:单独开端口后依然裸奔
有些团队用了独立管理端口方案:
management: server: port: 9090这样业务端口 8080 不暴露 Actuator,管理端口 9090 单独暴露。理论上挺好,但部署在外面加安全组的时候忘了限制 9090 的来源 IP,结果管理端口照样能被公网扫到。
记住:独立端口只是把攻击面挪了个位置,没有网络安全组配合,等于换了扇门还是没上锁。正确做法是管理端口的访问源限制到运维跳板机、监控系统所在的网段,再配合认证,双保险。
4.4 问题排查速查表
| 现象 | 可能原因 | 排查方式 | 解决方向 |
|---|---|---|---|
| 业务接口全部 401 | anyRequest()没放行 | 查看 SecurityFilterChain 配置 | 加anyRequest().permitAll() |
health匿名访问 401 | health 没加permitAll() | curl 查看返回码 | 单独放行 health |
| health 详情泄露内部信息 | show-details设成always | 匿名访问 health 看返回体 | 改为when-authorized |
| 带凭据访问还是 401 | 密码过期、环境变量注入失败 | 检查启动日志和配置中心 | 重新注入环境变量 |
| 网关无法访问 Actuator | 网关层把 Basic 头剥掉了 | 抓包或看网关日志 | 调整网关透传策略或改用 Token 头 |
| 某个敏感端点仍然 200 | exposure.include配了* | 访问对应端点确认 | 收拢 exposure 列表 |
| 响应特别慢 | heapdump 等端点被扫描器大量调用 | 看访问日志和线程栈 | 加 IP 白名单、限流 |
4.5 关于"已经裸奔过了怎么办"
如果确认生产环境的 Actuator 已经裸奔过一段时间,光加认证不够,还要做三件事:
- 轮换所有在配置里出现过的密码和密钥。假设它们已经被爬走,轮换是唯一有效的补救。
- 分析访问日志,看
env、heapdump、mappings有没有被非预期来源访问过。如果有,把时间范围内的访问 IP、UA、请求频率记下来,作为安全事件后续处理的依据。 - 检查有没有未授权的新用户或定时任务。裸奔状态下,攻击者可能已经通过 Actuator 修改了日志级别、打开过 shutdown 端点。这个只能靠日志和变更记录逐项核对。
5. 说句大实话:认证不是终点,还要做组合加固
5.1 最小暴露原则:哪些端点建议直接关掉
Spring Security 解决的是"谁能访问"的问题,但真正安全的架构是"根本不让你看到"。
我建议把下面这些端点直接设为不暴露:
env:配置属性泄露风险极高,除非调试需要,否则别开。configprops:同理,会把@ConfigurationProperties绑定的配置全部列出来。beans:暴露所有 Bean 类型和依赖关系,对攻击者做情报收集太友好。heapdump:堆内存里的敏感信息量太大,能不开就不开。threaddump:泄露业务线程调用栈和内部类名。mappings:完整接口地图,不开。shutdown:无论如何都不开。logfile:如果把日志级别、日志内容都暴露出去,等于给攻击者送内部情报。
需要保留的通常是health、info、prometheus,其中prometheus按监控系统是否需要决定。
配置方式:
management: endpoints: web: exposure: include: health,info,prometheus exclude: env,configprops,beans,heapdump,threaddump,mappings,logfile有人可能担心"那我以后排查问题怎么办"。正常排查走日志中心和监控系统,真到了需要临时开env的程度,也应该由运维临时改配置并加认证,而不是长期开着等别人来打。
5.2 组合拳:认证 + 独立端口 + 网络白名单
我推荐的生产配置是三层组合:
第一层,Spring Security 认证兜底,保证即使网络层被穿透,也拿不到端点数据。
第二层,独立管理端口,把业务端口和管理端口分离。这样可以针对管理端口做单独的网络安全策略,不会影响业务流量。
management: server: port: ${MANAGEMENT_PORT:9090}第三层,网络白名单。在云平台安全组或者自建防火墙里,只允许运维跳板机、监控系统所在网段访问管理端口。这一层配合前两层,即使有人拿到认证凭据,也不一定网络可达。
这套组合拳的逻辑是:认证防陌生人,网络白名单防内鬼,独立端口防业务攻击面外溢。
5.3 和监控系统衔接时要注意什么
如果团队用 Spring Boot Admin 或 Prometheus 抓指标,要注意几个细节:
- Spring Boot Admin 需要访问多个 Actuator 端点,比如
health、metrics、logfile、env。它自己会配置监控客户端账号,确保这个账号收到的端点数据是干净的,不要直接复用业务账号。 - Prometheus 抓取
/actuator/prometheus时,如果也配置了认证,要在抓取配置里带上 Basic Auth 凭据。别小看这个,很多团队给 Prometheus 配完认证后忘了更新抓取目标,结果监控一夜之间全部断掉。 - 给监控系统用的账号,权限要做到最小化。能看指标就别给配置写入权限,能读日志就别给
env。
5.4 我习惯做的三个日常自检动作
第一,每次发版前跑一遍无凭据扫描:
for endpoint in env configprops beans heapdump threaddump mappings logfile; do code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/$endpoint) echo "$endpoint -> $code" done预期所有端点都返回 401 或 404。返回 404 更好,说明根本就没暴露。
第二,检查配置文件有没有出现明文密码。不止是 Actuator 场景,而是所有配置项。可以用脚本或 IDE 扫描,把password、secret、key等关键词全部拉出来过一遍。
第三,确认health可以匿名访问,但详情只能认证后查看。这个我刚才已经提过,但值得单独作为自检项,因为它是唯一一个需要保持匿名可访问的端点,也是最容易被误配成泄露详情的地方。
我个人在实际操作中的体会是,Actuator 加认证这件事,只要按顺序做三步就稳了:收拢暴露端点、加 Spring Security 规则、独立管理端口加网络白名单。一次配好,后面基本不用再动。如果你现在正在生产环境紧急修这个问题,跳过花哨方案,先把 3.1 到 3.4 做了,至少能立刻挡住 99% 的扫描器。等这波缓过来,再回头补网络层和监控账号的细节。