高可用的服务注册中心:Eureka 集群搭建 , 并使用Spring Security 为 Eureka 增加 Basic 安全认证机制
上一篇 Spring Cloud 项目的搭建文章 : 链接
再说下我的版本号 , 用的比较新:
Spring Boot 2.0.5 RELEASE
Spring Cloud Finchley.SR3
项目管理工具: gradle
进入正题:
从之前的项目构造来看,RPC虽然是实现了,但是调用的链接确是写死在代码中的,比较丑陋。
要是提供服务的地址突然换了,那这边消费者直接雪崩,只能更改代码重新部署。而且无法实现负载均衡,这在一个微服务架构中是很不合理的。
要解决这问题,作为服务消费者,必须要有一个牛逼的服务发现机制。
Spring Cloud 就提供了多种服务发现组件的支持。Eureka、ZooKeeper 等,我这就用 Eureka 了, 在 Spring Cloud 这个生态圈里Eureka是比较流行的。
Eureka是网飞(Netflix)开源的服务发现组件。有Server和Client两部分,按需使用就行,我这就使用他们来构建一个高可用的服务注册中心。
我先把之前父模块的的Mybatis、mysql依赖给去了,放到了实际的微服务中(user/movie)。因为Eureka服务不需要这几个依赖,要是加载了mysql依赖会自动连接mysql,不加mysql的配置就会报一堆错误。
既然 Eureka 也是个服务,那当然也得在父模块下创建一个Eureka子模块。和其余的模块创建相同,没什么好说的。
给 Eureka 服务注册中心模块加上这个 Eureka Server 依赖
compile(“org.springframework.cloud:spring-cloud-starter-netflix-eureka-server“)
然后在 Eureka 服务的入口 java 文件上加上该注解 @EnableEurekaServer , 这个Application就是完全体了。
package com.skypyb.sc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer //表示这个服务是一个 Eureka 服务注册中心 public class EurekaServerApplication { public static void main(String[] args) { SpringApplication application = new SpringApplication(EurekaServerApplication.class); application.run(args); } }
此时可以在网页里通过 ip:port 来访问页面,可以得知注册中心的状态、多少服务链接到本机。
Eureka Server 端写完后,需要注册的服务则需要导入Eureka的Client依赖,就是这个:
compile(“org.springframework.cloud:spring-cloud-starter-netflix-eureka-client“)
然后只要在yml文件中配置好要连接的服务注册中心的地址就完事了,其实此时一个服务注册发现功能已经可以完成了。
但是还不够,需要考虑到若是 Eureka Server 端突然宕掉,那么整个系统就会出问题。
客户端虽然会有相应的缓存(服务注册时会将Server端储存的数据拿到本地缓存),但是在此时若是其他的服务发生变更,靠缓存中的信息去请求就不正确了。
所以,想要高可用的一个服务注册中心,则需要搞一个集群。宕掉一个,还有另外的,这样容错率将大大提高。
其实Eureka搞集群非常简单。不过是开启两个实例,A实例注册进B实例,B实例注册进入A实例而已。
只需要使用两种不同的配置即可。(你要是想开五个就是五个不同的配置,看着加就完事了)
像我这样,我这是写在一个yml文件里了,用“—”来隔开,要是配置比较多是可以另起一个文件的,就叫application-xxx.yml。
可以使用 –spring.profiles.active=xxx 来启动不同的服务。
(这里有部分Security的配置,下面就讲)
spring: profiles: active: eureka1 #启动时加上这个命令使用不同的配置 --spring.profiles.active=xxx security: #验证的用户名和密码 user: name: user password: 614 #Eureka Server 端配置 eureka: client: healthcheck: enabled: true service-url: defaultZone: http://user:614@localhost:8080/eureka/,http://user:614@localhost:8081/eureka/ --- spring: profiles: eureka1 application: name: sc-demo-microservice-eureka_1 server: port: 8080 eureka: instance: hostname: eureka1 --- spring: profiles: eureka2 application: name: sc-demo-microservice-eureka_2 server: port: 8081 eureka: instance: hostname: eureka2
在IDEA里边,可以使用这种方式来启动不同的实例:
集群也搭完了,启动第一个Eureka Server时可能会出点连接错误,不过不影响启动,这是因为第二个Server还没启动起来,而配置里需要它注册进去导致的,第二个启动起来后就没错误了。
你要是一下子就从网页进去看Eureka的状态,可能会出现红色粗体的:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
这个是Eureka的自我保护机制导致的。Eureka Server和Client之间每隔一段时间会进行一次心跳通信,告诉Server,Client还活着。要是某一段时间Server感觉自己这边少了几个心跳就会出这问题,只要不是真的你服务宕掉了,过一会就好了。
配置完毕后,客户端也只需要在配置文件中写两个链接就行,用逗号隔开。其实写一个也可以,因为Eureka集群会自动同步其他Eureka的注册信息。但还是推荐写俩,就是怕某些极端情况发生。
此时集群已经搭建完毕。客户端也可以连接得上,但是,这些端口就这样暴露出来也是很不科学的。攻击者可以轻易地得到你的服务信息。
最好的,当然还是加个验证啦!用户名密码这种验证永远不过时哈。
搭建完集群后,用 Spring Security 为 Eureka 增加 Basic 安全认证就很不错了!
只需要在 Eureka Server这边添加 Spring Security 的依赖就够了,Client端不用。
compile(“org.springframework.boot:spring-boot-starter-security“)
添加Security的依赖后就会自动开启验证,不用配置。用户名和密码的设定就像上边文件一样,只需要这样就行。
spring: security: #验证的用户名和密码 user: name: user password: 614
配置完用户名和密码后,连接Eureka Server自然就需要用到这些用户名和密码。
本来连接Server的的URL: http://ip:port/eureka/
现在连接Server的的URL: http://username:password@ip:port/eureka/
就算是从网页查看状态,也需要输入用户名和密码了哦。
Client端最终配置文件如下(反正配置都长得差不多,我这就放其中一个服务的文件就行了):
server: port: 8089 spring: application: name: sc-demo-microservice-movie #这个名字会注册到 Eureka 里去 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.8.113:3306/scdemo?serverTimezone=Asia/Shanghai username: root password: 614 #mybatis实体类名 mybatis: type-aliases-package: com.skypyb.sc.entity configuration: #到下划线的表字段自动映射成驼峰命名法 map-underscore-to-camel-case: true mapper-locations: classpath:mybatis/mapper/*.xml #Actuato 配置 management: endpoint: # 暴露shutdown功能 # shutdown: # enabled: true endpoints: web: exposure: include: '*' #暴露哪些端点 exclude: #隐藏哪些端点 #Eureka client端配置 eureka: client: service-url: defaultZone: http://user:614@localhost:8080/eureka/,http://user:614@localhost:8081/eureka/ instance: prefer-ip-address: true #将自己ip注册到Eureka Server
这样,其实应该就完事了,但是,在2.x版本的 Spring Security 中,引入依赖后,自动开启CSRF安全认证。
任何一次服务请求默认都需要CSRF 的token , 而我区区一个小Client,哪里来的这种东西。就是Eureka Server 自注册都会挂掉。
那么还是要让 Security 的CSRF拦截自动忽略掉注册Eureka的链接比较好。
在 Eureka Server 端增加这么一个配置类:
package com.skypyb.sc.config; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().ignoringAntMatchers("/eureka/**");//这个链接不使用csrf super.configure(http); } }
这样子去除 /eureka/ 这个连接的 CSRF 效验。就都可以连接上了。
OK 整体 Eureka 注册中心都搭建完毕了。并且实现了集群高可用和安全策略,比较完善。
服务之间的调用在做一点点微小的改进:
package com.skypyb.sc.controller; import com.skypyb.sc.entity.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController @RequestMapping("/movie") public class MovieController { private Logger logger = LoggerFactory.getLogger(MovieController.class); @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @GetMapping("/{id}") public User getUser(@PathVariable Long id) { //获取 user 服务的信息 List<ServiceInstance> instances = discoveryClient.getInstances("sc-demo-microservice-user"); String userUrl = instances.get(0).getUri().toString(); logger.info("access getUser method."); return restTemplate.getForObject(userUrl + "/user/" + id, User.class); } }
通过 DiscoveryClient 这个类,可以获取到对应服务的信息,这里我便是用 movie 服务获取到了user 服务的 url 来进行的调用。
当然,就算是这样,也不是那么的方便,关于feign声明式调用之后也会写篇文章的。
我的项目地址: github
2 COMMENTS