背景:
为SpringCloud微服务添加了Actuator依赖,为的是解决微服务项目启动顺序中,在上个服务的状态变为启动成功"UP"状态后,在启动下一个项目,不然nacos心跳检测会有异常报错。
可是在添加完 Actuator 并项目稳定运行后,甲方说通过渗透发现可以直接通过域名访问actuator下的其他所有接口, 例如
- /env
- /info
- /mappings
- /shutdown (很危险)
- ...
// 我们所期望的获取服务状态
curl "http://xxx/actuator/health"
// 返回
{
"status": "UP"
}
虽然我们只想要一个health功能,但是和health一起的还有很多其他支持方法,同时health我们也不想加上权限,所以想出来了一个应该比较好的解决样子。
期望
1 /actuator/health 可直接调用拿取返回值,无需验证
2 /actuatr/xxx 不可访问,或者需要登录
3 不影响其他业务使用,例如/login/xx 接口等
实现
通过查询类似功能一般是 Actuator + Security 结合使用,Gateway本身是控制不了 Actuator 相关接口的
所以该功能其实是Spring Cloud Gateway + Spring Actuator + Spring Security
1 gateway 中添加依赖
<!-- actuator 健康检测 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2 application 配置
# health 和 jms有相关关系
# - health 默认会返回服务里所有的健康(中间件等等)
# - jms 配置如下后,返回的值就相对简约,只有status
management.health.jms.enabled=false
# 账号密码
security.user.name=admin
security.user.password=xxx
# 开放的接口,禁用的接口
management.endpoints.web.exposure.include=health
management.endpoints.web.exposure.exclude=
# 不确定,似乎是因为我的webflux的原因导致冲突,推荐我添加该配置项
spring.main.allow-bean-definition-overriding=true
如果未禁用jms,health 返回的信息格式
{
"status": "UP",
"components": {
"jms": {
"status": "UP",
"details": {
"provider": "ActiveMQ"
}
}
}
}
3 gateway 配置 SecurityConfig。 不适用于Gateway!!!!后续会说(适用于其他微服务)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 允许无认证访问 /actuator/health
.antMatchers("/actuator/health").permitAll()
// 剩余的 /actuator/** 都不许访问
.antMatchers("/actuator/**").denyAll()
.and()
.httpBasic().disable()
// 禁用默认的登录表单
.formLogin().disable();
}
}
该代码书写完成后,项目启动,无论访问什么url,都会跳转到 /login 页面
通过查询和不断的注释代码,排查问题,发现是 Security 自己拥有一套属于自己的认证系统
他的用户名称是"user"
密码会在项目打印台打印出来
而自己配置了SecurityConfig还会去走 Security 的认证逻辑的原因就是SecurityConfig没被注册成功
打印详细日志并询问Gpt得到以下结论
o.s.w.s.adapter.HttpWebHandlerAdapter : [75e2975a-3] HTTP GET "/xx/xx?A=1"
o.s.w.s.adapter.HttpWebHandlerAdapter : [75e2975a-3] Completed 302 FOUND
o.s.w.s.adapter.HttpWebHandlerAdapter : [75e2975a-4] HTTP GET "/login"
o.s.w.s.adapter.HttpWebHandlerAdapter : [75e2975a-4] Completed 200 OK
从你的日志来看,问题仍然是 Spring Security 的默认行为,**当请求未通过认证时,会将用户重定向到默认的 `/login` 页面**。这在 WebFlux 环境中也是类似的。下面我们详细分析和解决问题。
你的项目中提到了 **Spring Gateway**。在 Gateway 中,
Spring Security 使用的是基于 **WebFlux** 的 `SecurityWebFilterChain`,
而不是传统的 `WebSecurityConfigurerAdapter`。
因此,`WebSecurityConfigurerAdapter` 的配置可能未完全生效,或者 Gateway 的默认行为覆盖了你的配置。
所以我们也就知道了Config没有注册成功的原因
我尝试将该配置 copy 到一个下属微服务中,通用的配置在其他服务里项目运行没有问题,实现了我所想要的效果,那么问题确认是 该方法在 gateway 中不适用,不适用的原因肯定是因为 SecurityConfig。
通过询问ChatGpt得到以下回复:
得出结论:Gateway中不可使用WebSecurityConfigurerAdapter,不生效
修改代码
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
// 关闭 csrf 校验
.csrf().disable()
.authorizeExchange()
// 允许 /actuator/health 可不认证
.pathMatchers("/actuator/health").permitAll()
// 禁止 /actuator/**,直接拒绝
.pathMatchers("/actuator/**").denyAll()
// 其他所有请求都允许通过
.anyExchange().permitAll()
.and()
// 禁用 HTTP Basic 认证
.httpBasic().disable()
// 禁用表单登录
.formLogin().disable();
return http.build();
}
至此,成功
依旧存在的问题:
1 A B 两个微服务中如果都开启了 Security,会导致fegin调用产生403异常。
而我们只开放了gateway端口,其他都是内网访问,所以除了Gateway的都没有添加 Security
2 文件上传暂时会有 “An expected CSRF token cannot be found” 异常,问题代看
Spring Security 默认会对 POST 请求进行 CSRF 校验