Eureka 是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud 将它集成在其子项目 spring-cloud-netflix 中,以实现 SpringCloud 的服务发现功能。
Eureka 包含两个组件:Eureka Server 和 Eureka Client。具体怎么部署这里就不说了,直接说问题
Eureka 客户端注册时需要配置服务端地址,类似如下配置
eureka:
instance:
hostname: hello-service
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${server.port}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: "http://localhost:8761/eureka/"
复制代码
这种配置后客户端就会注册到 Eureka 注册中心,在 Eureka 界面就能看到:
但是这样把界面暴露到外面,会把注册信息泄漏,一般公司也不允许暴露没有安全认证的后台界面
所以尝试把 Eureka 界面加密
引入 security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
复制代码
Eureka 服务端增加 basic 鉴权:
spring:
application:
name: eureka-server
security:
basic:
enabled: true
user:
name: admin
password: 123456
复制代码
配置完成后发现现在访问 eureka 界面需要用户名和密码登录了
但是登录进去后发现刚才的 hello 服务并没有注册进来
呕吼,应该是客户端没配置鉴权信息的原因,在官网找到了客户端鉴权配置方式
https://cloud.spring.io/spring-cloud-netflix/multi/multi__service_discovery_eureka_clients.html
于是在 hello 服务修改配置如下:
eureka:
instance:
hostname: hello-service
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${server.port}
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://admin:123456@localhost:8761/eureka/
复制代码
重启 hello 服务后,发现还是没有注册成功,原来增加 basic 验证后,不支持跨域访问了,我的天,你这个大坑,服务注册肯定是跨域的了,
于是,迅速增加配置,去掉跨域拦截
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.csrf().disable();//一种方式直接关闭csrf,另一种配置url后放行
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
}
复制代码
终于在界面看到可可爱爱的 hello 服务了,但是呢,还有一个问题,现在不允许这种明文密码出现在配置或代码中,怎么办呢?
首先想到的就是密码加密, 所以从 spring-securiry 中找到 PasswordEncoderFactories 加密
PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456")
复制代码
然后把加密后的结果放到 Eureka 服务端配置文件中:
security:
basic:
enabled: true
user:
name: admin
password: '{bcrypt}$2a$10$mhH7ogkRB91YDUO3F883JugDMHz2o6miT95.8ukqEc6Ed4Z2xyHmm' //必须有引号
复制代码
那 Eureka 客户端怎么办? 同样道理的嘛
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://admin:{bcrypt}$2a$10$mhH7ogkRB91YDUO3F883JugDMHz2o6miT95.8ukqEc6Ed4Z2xyHmm@localhost:8761/eureka/
复制代码
但是呢,启动直接报错,害,eureka 注册时并不会解密
解析 Eureka 服务端地址失败,即使是加上引号也是报相同错误
Eureka 客户端注册过来的消息,服务端并不会给解密,那怎么办呢?
从上图可以看到如果要实现更复杂的需求,需要通过注入 clientFilter 方式,so,搞起来
@Configuration
@Priority(Integer.MIN_VALUE)
public class UserCilentFilter extends ClientFilter {
@Override
public ClientResponse handle(ClientRequest clientRequest) throws ClientHandlerException {
try {
String originUrl = clientRequest.getURI().toString();
if(originUrl.contains("@")){
return this.getNext().handle(clientRequest);
}
String userNameAndPwd = "http://admin"+ jiemi("'{bcrypt}$2a$10$mhH7ogkRB91YDUO3F883JugDMHz2o6miT95.8ukqEc6Ed4Z2xyHmm'");
String addUserInfoUrl = originUrl.replaceFirst("http://", userNameAndPwd);
clientRequest.setURI(new URI(addUserInfoUrl));
} catch (URISyntaxException e) {
// FIXME: 2021/4/2
}
return this.getNext().handle(clientRequest);
}
private String jiemi(String pwd) {
// FIXME: 解密
return pwd;
}
@Bean
public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs = new DiscoveryClient.DiscoveryClientOptionalArgs();
discoveryClientOptionalArgs.setAdditionalFilters(Collections.singletonList(new UserCilentFilter()));
return discoveryClientOptionalArgs;
}
}
复制代码
这样写的思路是让其他客户端注册时去掉用户名和密码,然后在自定义过滤器中对没有用户名和密码时补充上 basic 验证的用户名和密码
然后开始测试,这样还是不行,其他服务注册过来时,会被其他安全过滤器拦截都走不到自定义的拦截器就返回鉴权失败了,即使 @Priority(Integer.MIN_VALUE)最高优先级
那是不是可以在更前面的地方进行拦截呢?增加 ServletRequest 拦截器可行否?
@Configuration
public class ServerRequestAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
//业务实现,根据请求的IP或者参数判断是否可以执行注册或者访问
// String addUserInfoUrl = originUrl.replaceFirst("http://", "http://admin:123456@");
filterChain.doFilter(request, response);
}
}
复制代码
但是假设这样修改后,登录的 web 界面也会走到这个拦截器,同样会增加鉴权
也就是说这样直接增加鉴权,无法区分是其他客户端注册还是从界面访问
也没有什么太好的办法了,就直接在 security 的拦截器中把 eureka 注册相关的放掉,不进行鉴权操作
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/eureka/**").permitAll();
super.configure(http);
}
复制代码
这样设置后除了直接访问的界面需要鉴权外,其他 eureka 相关注册、查询等不需要鉴权
都这样分层鉴权操作了,再找下是不是有其他方式达到相同的目的,于是找到
eureka:
dashboard:
enabled: false
复制代码
通过在启动脚本设置后,效果是类似的,在 eureka 主界面无法访问
上面所有的操作都是为了信息安全考虑,还有一个经常忘记需要考虑的组件是 Spring Boot Actuator,针对 Spring Boot Actuator 提供的 endpoint,采取以下几种措施,可以尽可能降低被安全攻击的风险
最小粒度暴露 endpoint。只开启并暴露真正用到的 endpoint,而不是配置:management.endpoints.web.exposure.include=*。
为 endpoint 配置独立的访问端口,从而和 web 服务的端口分离开,避免暴露 web 服务时,误将 actuator 的 endpoint 也暴露出去。例:management.port=8099。
引入 spring-boot-starter-security 依赖,为 actuator 的 endpoint 配置访问控制。
慎重评估是否需要引入 spring-boot-stater-actuator。以我个人的经验,我至今还没有遇到什么需求是一定需要引入 spring-boot-stater-actuator 才能解决,如果你并不了解上文所述的安全风险,我建议你先去除掉该依赖。
信息安全已经成为各大公司不得不考虑的问题,所以精准的权限控制也是必不可少的,希望本文对大家在使用 SpringCloud 相关组件安全控制上有启发作用。
如果觉得俺写的还可以,记得点赞,一键三连也不介意。
☞☞每周一篇,赛过神仙,看完点赞,养成习惯☜☜
评论