前言
本文记录了最近几天在下对于spring cloud的摸索使用阶段所遇到的一些坑,以及坑的解决过程。过程中参考了包括spring官方的文档,和各路大神们的blog等等,参考的部分我会使用直接上原链接的方式而不是大段地引用过来。
目前是只搭建好了后端的分布式环境,但是网关暂时还没加进去,同时前后端交互的问题在下也还没有解决(嘤嘤嘤前端除了JS之外瓦塔西瓦完全不懂desu~),找前端小伙伴问了问说是最近比较流行vue.js(学前端是不可能的,这辈子都不想再碰前端;真香!),后续看缘分能不能出(二)吧。
本文基于 spring boot 2.1.5.RELEASE 版本
那么下面开始我的踩坑记录。
How to begin?
这一节是专门写给萌新看的。
众所周知,石原里美是我老婆(大雾)咳咳,应该说,众所周知,spring 全家桶系列现在已经可以很方便地用spring boot集成各种组件了,除了一些参数的配置之外开发者可以说是非常省事。
想要开始搭建一个spring boot项目,有以下两种方式:
- 通过官方提供的 start.spring.io 页面,直接通过简单的选项即可生成一个完整的项目压缩包,下载解压后可以直接用IDE打开。
过程就不展开了,完全傻瓜式操作,请自行摸索。 - 如果你是用的intelj idea的话,终极版用户也可以直接new project的时候使用里面的spring initializer;社区版用户则可以下载一个名为spring assistant插件。用法与1基本一致。
在本文中所提到的各项功能会应用到不同的依赖,生成项目的时候需要在dependencies里选的,现在这里列举一下,后面讲不同模块的时候也会再说明:
- Eureka server 中心+安全验证:Cloud Discovery -> Eureka Server ,Security -> Security
- Eureka client端的服务发现与注册+安全验证:Cloud Discovery -> Eureka Discovery ,Security -> Security ,Web -> Web
- Ribbon 负载均衡与远程调用:Cloud Routing -> Ribbon
- Feign 负载均衡与远程调用:Cloud Routing -> Feign
(P.S.使用网页生成压缩包的小伙伴请忽略箭头前的部分,直接在Denpendencies上写出包的名字就ok了)
Eureka Server高可用集群中心的搭建
中心搭建需要依赖 Cloud Discovery -> Eureka Server 这个包
mvn build成功后在main类加入注解@EnableEurekaServer
just like this
@SpringBootApplication
@EnableEurekaServer
@ComponentScan("com.borris")
public class SpringCloudTiyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudTiyApplication.class, args);
}
}
如此即可在启动的时候自动化配置eureka server
集群中心的搭建参考官方文档的此处:
spring-cloud-eureka-server-peer-awareness
但实际配置时不能完全照搬文档上的内容,这么几个注意点:
spring:
profiles: center3
application:
name: eureka-center
eureka:
instance:
hostname : localhost:7001
client:
service-url:
defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/
server:
#设置健康节点检测间隔(ms)
eviction-interval-timer-in-ms : 10000
如上,文档中hostname用的是域名,如果本地也想用域名作为配置的话需要修改hosts文件,不想改Host的话直接写ip:port这样的形式就好了
然后配置defaultZone的时候,把多个server的url写上去,形式为
http://hostname/eureka
即可,中间以逗号隔开
配置成功之后,当中心启动起来时会看到页面上显示以下内容:
配置成功效果图
如上图,配置成功后会看到DS Replicas中显示集群的hostname,
同时会显示已注册的Application,名称为上面的spring.application.name
下面那块则不需要过多关注,defaultZone配置好了会显示一样的内容的,但真正上两块才是真正注册成功的显示
根据文档中所说,多个yml文件可以合并在同一个文件里,中间用【---】分隔开,以spring.profiles属性作为区分,启动时在program arguments加入
--spring.profiles.active=profileName
即可启动多个实例,而不需要每个实例改文件内容(方便本地测试用)
server端还可开启一个节点健康监测的选项,如上的
eureka.server.eviction-interval-timer-in-ms
属性,例如我这里设置的是每10s(10000ms)检查一下节点健康,当检查到节点工作状态不正常会自动从列表上删除
Eureka Client搭建方法
Eureka Client端需要依赖 Cloud Discovery -> Eureka Discovery ,Web -> Web 两块
client端需要在main类加入注解@EnableDiscoveryClient,跟上面一样就不重复贴代码了
client端的配置方式基本上与server端类似其实,请查看下面的配置
eureka:
client:
#表示eureka client间隔多久去拉取服务器注册信息,默认为30秒
registry-fetch-interval-seconds : 5
service-url:
defaultZone : http://localhost:7001/eureka/
instance:
#心跳间隔
lease-renewal-interval-in-seconds : 5
#心跳停止后的节点过期时间
lease-expiration-duration-in-seconds : 10
instance-id : localhost:${server.port}
参考上面的注释和属性,在client端的角度上来说,defaultZone就是它们的注册节点,基本上来说,如果server中心的配置和工作都是正常的话,那么client只注册单个server,server集群会自动把所有的注册信息复制到其他的endpoint上,因此也可以通过client端的注册状态,来验证server集群的配置是否正确
client注册效果图如上图所示,虽然我的client的url只配置了7001这一台server,但是因为server是集群工作的,所以我可以登陆7002和7003也同样看到上图中test-client的注册信息
但是保险起见,为了避免单个endpoint注册时发生节点宕机或其他的风险,实际生产上运用的时候还是将所有endpoint的配置都配全比较好,多个url之间用逗号隔开,如下:
defaultZone : http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/
另外有一个很奇怪的点,目前观察发现注册的url默认是绑定到了 /eureka 上面去,我觉得应该是可以提供修改的property的,但实际上我查找了文档以及各路大神的blog,发现貌似是定死了不能改的,我只发现了一个文档上一个疑似的
eureka.instance.namespace | eureka | Get the namespace used to find properties. Ignored in Spring Cloud. |
---|
看这个property的描述,【获取用于查找属性的命名空间,但在spring cloud中被忽略】
另外还在源码中找到了以下内容:
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
/**
* Register the Jersey filter.
* @param eurekaJerseyApp an {@link Application} for the filter to be registered
* @return a jersey {@link FilterRegistrationBean}
*/
@Bean
public FilterRegistrationBean jerseyFilterRegistration(
javax.ws.rs.core.Application eurekaJerseyApp) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
//使用了一个常量EurekaConstants.DEFAULT_PREFIX 注册filter的url拦截
bean.setUrlPatterns(
Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*"));
return bean;
}
让我们再来看看这个EurekaConstants.DEFAULT_PREFIX 是个什么来头
org.springframework.cloud.netflix.eureka.EurekaConstants
/**
* Default Eureka prefix.
*/
public static final String DEFAULT_PREFIX = "/eureka";
emmmmm不大明白为什么eureka的注册filter要这样设计一个写死的url,但看到这一行我就知道可以死了心了,目前官方不提供修改服务绑定名的途径,如果不想自己重构的话就先将就着用吧。
另外client端多节点部署的话,可以直接参考server的多节点搭建,这里就不赘述了。
加入Security安全验证
spring cloud 支持client与server之间使用安全验证进行注册,在建立server(client不需要)项目的时候加入Security -> Security 依赖即可,也可以直接在pom.xml上加上
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
加入后可以在yml或者properties里加入用户名和密码
spring:
security:
basic:
#打开安全开关
enabled: true
#用户名密码
user:
name: name
password: password
加入这些配置之后重新启动项目,访问中心的时候就会出现登陆画面
登陆画面
登陆后即可看到前面部分所提起过首页
client端的配置变化其实也不大,请参考下面的URL配置:
eureka:
client:
#表示eureka client间隔多久去拉取服务器注册信息,默认为30秒
registry-fetch-interval-seconds : 5
service-url:
defaultZone : http://name:password@localhost:7001/eureka/,http://name:password@localhost:7002/eureka/,http://name:password@localhost:7003/eureka/
如上,只需要将defaultZone的部分加入name:password在中间即可
特别注意:
目前的eureka client比较坑的一点是,它会自动化配置CSRF防御机制,然而eureka client并没有加入对其的支持,根据spring 文档(spring-security#csrf)称
to ensure that you include the CSRF token in all PATCH, POST, PUT, and DELETE methods
根据此处可以得知,spring security会对上述的http method都认为是有风险的,而如果这些method发送过程中没有带上 CSRF token的话,会被直接拦截并返回 403 forbidden,下面让我们来瞅瞅client是怎么给server发送注册请求的,client启动过程打出的异常日志中,可以跟踪到接收403信息的类,往下跟踪,在下找到了这个类:
com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient
我们来看看它的register方法:
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
细心的小伙伴应该注意到了,它的请求是POST粗去的,而且前面并没有请求获取 CSRF token 的步骤,所以毫不意外地被server给forbidden了。
按理说这个问题已经出现了很久了,因为我最早看到有人用spring boot 1.5.x的版本已经有这个问题。至于官方为什么不维护eureka client修复这个问题呢?咱也不知道,咱也不敢问……
但是官方还是给出了解决的方法,具体可以参考 spring cloud issue 2754,里面有大量的讨论,我这边总结的解决方案:
- 配置一个@EnableWebSecurity配置类,继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,重写configure方法。
重写有两种方法,方法①如下:
1). 使CSRF忽略 /eureka/*的所有链接
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
2). 保持密码验证的同时禁用CSRF防御机制
@Override
protected void configure(HttpSecurity http) throws Exception {
//注意,如果直接disable的话会把安全验证也禁用掉
http.csrf().disable().authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
- 我个人觉得其实还有另一个方法,自己重构register方法,使其在发送注册请求前先GET获得一个CSRF token,后续再POST注册请求,但是这个方法改如何实现emmmmmm目前还没有这个想法去搞(你个死肥宅就是懒得动)
使用上面的方法1之后,在下这边是可以正确注册上了,如果有哪位小伙伴还是不行的话,可以留言或者私信我,我有空的话一起来看看是什么问题~
本文暂时到这里,后续我会考虑把Feign或者Ribbon这两种远程调用的方式测试对比,然后写初见(2)的总结,有余力的话考虑加入Zuul网关?
那么有缘再会~
20190606
神驱一梦
于无月之夜
(P.S.夹个私货,写结尾时的BGM是勾指起誓,在下非常喜欢~ 请务必吃下这个安利~)
网友评论