Spring Cloud Config远程分布式配置
一、Spring Cloud Config介绍
可能有人已经听说过 Spring Cloud Config,但分布式配置解决方案却不止 Spring Cloud Config,还有其它一些框架,例如 360 的 QConf 、淘宝的 diamond 、百度的 disconf 等都可以解决分布式配置中心问题。国外也有很多开源的配置中心例如 Apache Commons Configuration 、owner 、cfg4j 等等,但是 Spring Cloud Config 的功能更为强大,而且可以和 Spring 家族无缝结合,非常方便。
Spring Cloud Config 项目是一个解决分布式系统的配置管理方案。它包含了 Client 和 Server 两个部分,Server 提供配置文件的存储,然后以接口的形式将配置文件的内容提供出去,Client 通过接口获取数据,然后依据此数据初始化自己的应用。Spring cloud 支持使用 Git 或者 Svn 存放配置文件,默认情况下使用 Git。
那么Spring Cloud Config 有哪些功能?
- 提供服务端和客户端的支持
- 集中管理各种环境的配置文件
- 配置文件修改后,可以快速生效
- 因为配置文件通过 Git 或者 Svn 进行管理,所以配置文件天然具备版本回退等功能
- 支持大的并发查询
- 支持多种开发语言
首先我们在码云(https://gitee.com)创建一个公共仓库名字cloud-config-repo备用,然后初始化本地仓库:
- 创建文件夹gitee
- 进入gitee文件夹,右键Git bash here进入控制面板,执行克隆项目命令:git clone "你的仓库地址",这样gitee目录下会多出一个cloud-config-repo文件夹,里面有个.git
我们进入cloud-config-repo创建cient1目录,clien1目录下创建三个不同的文件,分别代表开发环境、测试环境、生产环境的配置文件:
client1-dev.yml:
process: name: java
client1-test.yml:
process: name: c++
client1-prod.yml:
process: name: SpringCloud
这样创建好后,我们使用git命令上传到我们之前创建的仓库。
git add . git commit -m "微服务配置" git remote add origin 您的github仓库地址 git push -u origin master
二、Config Server
Spring Cloud Config 的运作原理图:
ConfigServer 从 Git 仓库获取到配置文件,然后 ConfigClient(就是一个一个的微服务) 再从 ConfigServer 中获取各自的配置文件,大致就是这样一个工作流程。
添加依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </dependencies>
创建好后,然后在启动类上加入注解@EnableConfigServer。
在application.yml中配置远端仓库配置:
spring: application: name: config-server cloud: config: server: git: uri: https://gitee.com/xxxx/cloud-config-repo.git search-paths: client1 # 搜索的路径 username: 码云用户名 password: 码云密码 server: port: 8081
配置说明:
uri 代表github仓库地址,就是存放您远端配置信息。
search-paths 表示仓库中配置文件的地址,由于 ConfigServer 是 Git 仓库和微服务之间的一个桥梁,不同微服务的仓库在 Git 仓库中放在不同的目录下,这里是指具体的目录,这里是client1开始。
接下来两个配置就是自己的 码云 的用户名和密码,但是这两个配置并非必须的,如果你的仓库就是公开的,那么可以不必配置,实际生产环境中,仓库往往是私有的,因此这里就需要配置用户密码来获取仓库的访问权限。
完成后,我们启动项目,然后访问http://localhost:8081/client1/dev/master
然后返回如下信息则成功:
{ "name": "client1", "profiles": ["dev"], "label": "master", "version": "874be8e42c7c589fabca3f58c14f2862ad68353f", "state": null, "propertySources": [{ "name": "https://gitee.com/xxxxxx/cloud-config-repo.git/client1/client1-dev.yml", "source": { "process.name": "java" } }] }
同时发现idea的控制台输出了如下日志:
Adding property source: file:/C:/Users/ADMINI~1/AppData/Local/Temp/config-repo-7504535007002169434/client1/client1-dev.yml
进入这里提到的目录,发现 config server 已经将远程 git 仓库中的内容 clone 下来了。
事实上,仓库中的配置文件会被转换成web接口,访问可以参照以下的规则:
- /{application}/{profile}[/{label}]
- /{application}/{profile}.yml
- /{application}/{profile}.properties
- /{label}/{application}-{profile}.yml
- /{label}/{application}-{profile}.properties
这里,各个占位符含义分别如下:
- {application} 表示配置文件的文件名
- {profile} 表示配置文件的 profile ,例如我们上文提到的 test、dev 以及 prod
- {label} 表示配置文件的 git 分支
此时我们尝试修改本地仓库的配置文件,例如修改 client1-dev.yml 文件内容,然后提交到 Git 仓库,再通过 ConfigServer 进行访问,发现访问到的数据已经发生了变化,说明 ConfigServer 中的数据可以实时更新。
三、Config Client
创建一个Config Client的子模块,添加以下依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> </dependencies>
创建成功后,添加配置文件,注意配置文件添加在 bootstrap.yml 文件中,这时需要大家手动在 classpath 下添加 bootstrap.yml配置文件,相对于 application.yml文件,bootstrap.yml文件加载时机更早,适合于目前的场景,bootstrap.yml中的配置内容如下:
spring: application: name: client1 cloud: config: profile: dev label: master uri: http://localhost:8081/ server: port: 8082
配置说明:
- 前两个是应用名称和端口号。
- 上文我们提到的访问规则是
/{application}/{profile}[/{label}]
,当 Config Client 去访问 Config Server 时,spring.application.name
、spring.cloud.config.profile
以及 、spring.cloud.config.label
的值分别对应上面三个占位符,所以这三个地方不能乱写,要根据实际情况来,特别是spring.application.name
不能写错,因为之前我们定义名称时都是可以随意取的,这里不可以。 - spring.cloud.config.uri 则表示 config server 的地址。
配置完成后,当我们的 config client 项目启动时,配置文件会被自动加载到项目中,我们就可以像使用普通配置文件一样来使用仓库中的配置文件,我们写一个简单的 HelloController 来测试下,如下:
@RestController public class HelloController { @Value("${process}") private String process; @GetMapping("/process") public String process() { return process; } }
配置完成后,分别启动configServer和configClient(启动时可以看到日志: Fetching config from server at : http://localhost:8081/),然后访问控制器地址http://localhost:8082/process。
成功访问到我们需要的配置文件,如果想访问 client1-prod.yml,只需要修改 bootstrap.properties 中 spring.cloud.config.profile
的值为 prod 即可。
其他配置方式
前面和大家说过,spring.application.name 、spring.cloud.config.profile 以及 spring.cloud.config.label 的值分别对应 {application} 、 {profile} 以及 {label} 三个占位符,即在 config_server 中,我们可以通过 {application} 、 {profile} 以及 {label} 分别访问到 config client 中对应的 spring.application.name 、 spring.cloud.config.profile 以及 spring.cloud.config.label 的值。利用这种特性我们就可以实现对 config server 中 search-paths 属性的动态配置,例如我们可以将 search-paths 的值和 config client 的 spring.application.name 绑定在一起,这样就可以实现动态修改配置文件的文件夹了。例如我们上文提到的配置方案,我们现在可以直接修改 config server 的配置文件,如下:
git: uri: https://gitee.com/xxxxxx/cloud-config-repo.git search-paths: ${application} # 搜索的路径 username: password:
这里我们主要是修改了 search-paths 的值,将之用一个 {application}占位符代替,然后重启 config server ,发现运行效果和前面的一样。
如此之后,我们就可以通过灵活使用 {application} 、{profile} 、{label} 三个占位符,来动态地从 client 中控制 server 所访问的仓库了。读者可以根据自己的实际情况,把这三个占位符玩的更加精彩。
四、Config配置文件的加密和解密
常见加密方法
从整体上来说,加密分为两大类:
- 不可逆加密
- 可逆加密
不可逆加密就是大家熟知的在 Spring Security 或者 Shiro 这一类安全管理框架中我们对密码加密经常采取的方案。这种加密算法的特点就是不可逆,即理论上无法使用加密后的密文推算出明文,常见的算法如 MD5 消息摘要算法以及 SHA 安全散列算法, SHA 又分为不同版本,这种不可逆加密相信大家在密码加密中经常见到。
可逆算法看名字就知道,这种算法是可以根据密文推断出明文的,可逆算法又分为两大类:
- 对称加密
- 非对称加密
对称加密是指加密的密钥和解密的密钥一致,例如 A 和 B 之间要通信,为了防止别人偷听,两个人提前约定好一个密钥。每次发消息时, A 使用这个密钥对要发送的消息进行加密,B 收到消息后则使用相同的密钥对消息进行解密。这是对称加密,常见的算法有 DES、3DES、AES 等。
对称加密在一些场景下并不适用,特别是在一些一对多的通信场景下,于是又有了非对称加密,非对称加密就是加密的密钥和解密的密钥不是同一个,加密的密钥叫做公钥,这个可以公开告诉任何人,解密的密钥叫做私钥,只有自己知道。非对称加密不仅可以用来做加密,也可以用来做签名,使用场景还是非常多的,常见的加密算法是 RSA 。
配置文件加密肯定是可逆加密,不然给我一个加密后的字符串,我拿着也没用,还是没法使用。可逆算法中的对称加密和非对称加密在 Spring Cloud Config 中都得到支持。
对称加密
Java 中提供了一套用于实现加密、密钥生成等功能的包 JCE(Java Cryptography Extension),这些包提供了对称、非对称、块和流密码的加密支持,但是默认的 JCE 是一个有限长度的 JCE ,我们需要到 Oracle 官网去下载一个不限长度的 JCE :不限长度JCE下载地址
下载完成后,将下载文件解压,解压后的文件包含如下三个文件:
将 local_policy.jar 和 US_export_policy.jar 两个文件拷贝到 JDK 的安装目录下,具体位置是 %JAVA_HOME%\jre\lib\security
,如果该目录下有同名文件,则直接覆盖即可。
然后在config server的resource下创建一个bootstrap.yml,
并且增加如下配置:
encrypt: key: 666666
这里我们配置了我们的密钥为666666, 然后我们可以直接在浏览器去访问下面这个接口来检查我们的配置是否正确http://localhost:8081/encrypt/status。如果看到访问这个接口返回的是ok,说明是没有问题的。
我们使用post方法来访问http://localhost:8081/encrypt
加密接口:
“java”加密返回后内容如下:
fad9cef6c0f06554bc8e8c312611e6a1ba546f9302c9c147f66ad2b654c234bd
拿到加密后的字符串后,我们肯定要进行解密,那么我们访问解密接口http://127.0.0.1:8081/decrypt,传入已经加密的字符串,然后得到解密的字符串。
我修改本地仓库中的client1-dev.yml配置文件。将加密的字符串拷贝进来如下:
process: name: '{cipher}fad9cef6c0f06554bc8e8c312611e6a1ba546f9302c9c147f66ad2b654c234bd'
注意{cipher}
不要忘记了。只有加上它我们才能解密。
修改完成后,我们需要同步到github仓库。
我们重新访问Config Client的接口http://localhost:8082/process,返回的内容:
可以发现原来我们的配置文件是一串加密后的文本,现在变成可具体的文本,说明我们的远端解密完成了。
非对称解密
我们也可以使用非对称加密的方式来对配置文件进行加密,非对称加密要求我们先有一个密钥,密钥的生成我们可以使用 JDK 中自带的 keytool。keytool 是一个 Java 自带的数字证书管理工具 ,keytool 将密钥(key)和证书 (certificates) 存在一个称为 keystore 的文件中。具体操作步骤如下:
首先打开命令行窗口(或进入到%JAVA_HOME%/bin目录),输入如下命令:
keytool -genkeypair -alias config-server -keyalg RSA -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" -keypass 123456 -keystore config-server.jks -storepass 123456
参数解释:
- -genkeypair 表示生成密钥对
- -alias 表示 keystore 关联的别名
- -keyalg 表示指定密钥生成的算法
- -keystore 指定密钥库的位置和名称
执行过程中,密钥库口令需要牢记,这个我们在后面还会用到。其它的信息可以输入也可以直接回车表示 Unknown ,自己做练习无所谓,实际开发中还是建议如实填写。好了,这个命令执行完成后,在D:\\allspace\\路径下就会生成一个名为 config-server.keystore 的文件,将这个文件直接拷贝到 config server 项目的 classpath 下,如下:
然后在 config server 的 bootstrap.yml 文件中,添加如下配置(注意注释掉对称加密时的那一行配置):
encrypt: key-store: location: classpath:/config-server.jks alias: config-server password: 123456 secret: 123456
配置完成后,重新启动 config server 。启动成功后,加密解密的链接地址和对称加密都是一样的,因此,我们可以继续 http://localhost:8081/decrypt
对文本进行加密:
“java”加密返回后内容如下:
AQAGRQnH01JpfW3iy5bWiSn3/oWUh8rOypWKyz2x/UQ7wl3xoWOXExT0DndbZJwDRN3z+SJge85022cPybZL9k2knPtOxDlL9HSkoZkGUkQNDyaCHq79lUqHr81fZsN1cyODVRWD0Ffbq8SbBoZifJ3zMho9+/n5yJC1EyrPEQM/hVDXTkqmk7oahsdPTDSgtZ0wgu5eoAzauj097ca51LvSmvvmxUrtRUXOS8tYHpgu5maebtlgbAUwruU03TzsOopxrQ/db+mqzFTNgHF79GHkXDm5b1viX9/pyCFH7k70BtEscvKW0j128AJzyXDAhYnw5ByxU5RNF3VBNKJW76/3n9tRcz86WyrWwmSth2klHEkHgUkQ64RzhbWi9wPuCRo=
修改本地的client1-dev.yml配置文件,加密的配置如下:
process: name: '{cipher}AQAGRQnH01JpfW3iy5bWiSn3/oWUh8rOypWKyz2x/UQ7wl3xoWOXExT0DndbZJwDRN3z+SJge85022cPybZL9k2knPtOxDlL9HSkoZkGUkQNDyaCHq79lUqHr81fZsN1cyODVRWD0Ffbq8SbBoZifJ3zMho9+/n5yJC1EyrPEQM/hVDXTkqmk7oahsdPTDSgtZ0wgu5eoAzauj097ca51LvSmvvmxUrtRUXOS8tYHpgu5maebtlgbAUwruU03TzsOopxrQ/db+mqzFTNgHF79GHkXDm5b1viX9/pyCFH7k70BtEscvKW0j128AJzyXDAhYnw5ByxU5RNF3VBNKJW76/3n9tRcz86WyrWwmSth2klHEkHgUkQ64RzhbWi9wPuCRo='
修改完成后,我们需要同步到github仓库。
我们重新访问Config Client的接口http://localhost:8082/process,返回的内容:
这样,我们就完成了非对称解密了。
安全管理
目前的 config server 存在很大的安全隐患,因为所有的数据都可以不经过 config client 直接访问。出于数据安全考虑,我们要给 config server 中的接口加密。在 Spring Boot 项目中,项目加密方案当然首选 Spring Security ,使用 Spring Security 也很简单,只需要在 config server 项目中添加如下依赖即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
五、Config 的动态刷新、重试、服务化
假如说我们的配置从远程仓库获取失败了,那么该如何去处理呢?这里就要使用到 Spring Cloud Config 为我们提的动态刷新重试功能了,Spring Cloud Config 是服务化的。那么什么是服务化呢?
服务化
我们在前面的配置中,当 Config Client 需要从 Config Server 上获取配置数据时,我们都是直接在 Config Client 的配置文件中写上 Config Server 的地址,类似下面这种架构:
这种写法相当于将 Config Client 和 Config Server 绑定死了,以后 Config Server 的地址不能变,Config Server 也不能挂,否则 Config Client 就获取不到信息了,而且这种方式也破坏了我们微服务的整体架构,即服务之间互相调用,获取对方的信息都是去服务注册中心上获取,所以我们要对这种结构进行改造,改造成下面这种结构:
当 Config Server 启动时,将自己注册到服务注册中心 Eureka 上,所有 Config Client 都从 Eureka 上去获取 Config Server 的信息,这样我们就成功将 Config Server 和 Config Client 解耦了,Eureka 在这里依然扮演了数据中心的角色。
首先我们依然是创建一个cloudConfig-fwh普通maven工程来作为父工程。然后再从cloudConfig-fwh中创建一个普通的文件夹configRepo来存放github配置文件,然后再分别创建eureka、config_server、config_client。
创建好后,我们分别将config_client 和 config_server 注册到eureka实例上。
我们还需要对Config Client配置,这里我们在bootstrap.yml中配置,bootstrap.yml优先级比application.yml高,spring cloud config 优先配置都会放在这里:
bootstrap.yml 配置如下:
spring: application: name: config-server cloud: config: profile: dev label: master discovery: service-id: config-server3 enabled: true server: port: 8002 eureka: client: service-url: defaultZone: http://localhost:7000/eureka/
这里新增的两个配置,其中discovery.service-id
代替了原来的cloud.config.uri
原来的uri需要写很长。而且如果ip地址端口号发生了变化,那么还需要去Config Server去修改,这里使用了service-id完美了解决了这个问题。通过service-id去eureka中心寻找Config Server的实例。discovery.enabled=true
是开启通过eureka来获取Config Server。
注意:这个spring.application.name的名称是你在github仓库的配置文件的前缀。
动态刷新
当 Git 仓库中配置文件发生改变后,如果我们刷新 Config Server 中的请求地址,会发现数据也跟着变化了,即 Config Server 是能够及时感知到配置文件的变化的,但是这种感知却不能够传递到 Config Client 中去,即 Config Client 是无法及时感知到配置文件的变化的,默认情况下,只有 Config Client 重启,才能够加载到最新的配置文件数据,如何让 Config Client 也能动态刷新配置数据呢?
我们只需要在Config Client 中加入如下依赖就能动态刷新配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
添加完成后我们还需要对refresh接口暴露,这里注意,除了G版本的Cloud需要额外手动的暴露refresh接口外,其它版本的Cloud不用配置下面这段配置来进行手动暴露
management: endpoints: web: exposure: include: 'refresh'
配置好了后,我们对HelloController增加一个注解@RefreshScope
当调用refresh
接口当时候动态刷新:
@RestController @RefreshScope public class HelloController { @Value("${process.name}") private String process; @GetMapping("/process") public String process() { return process; } }
重启Config Client项目,然后可以看到idea的控制台/actuator/refresh
接口已经暴露出来了。
请求重试失败
请求失败了肯定要重试啊,不可能失败了,就让它一直失败。这肯定是不行的。比如网络的波动,当网络质量很差的情况下,就会导致服务调用的失败。那么我们就要做到请求失败了,就要重试。
要实现失败重试也是非常简单的,这里需要加两个依赖:
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
加上这个依赖后,我们需要在Config Client 的bootstrap.yml中加入如下配置:
fail-fast: true
这个配置的意思是失败快速响应,在默认情况下 我们的Config Client 去访问Config Server 失败时候,并不会马上报错 而是要等到使用到Config Server 的某个数据的时候才会报错,通俗的意思就是我们之前不是有个process.name变量吗?如果这个process.name变量并不存在,而我们的Config Client又在调用使用的话,那么就会报错并抛出异常。所以当我们的Config Client 访问 Config Server 失败的时候,就要开启快速响应,这里可以是失败重试,也可以抛出自定义异常信息。
配置请求策略:
spring: cloud: config: retry: initial-interval: 1000 multiplier: 1.1 max-interval: 2000
- max-attempts 表示最大请求次数,默认值为 6 。
- initial-interval 表示请求重试的初始时间间隔。
- multiplier 表示时间的间隔乘数,由于网络抖动一般都是有规律的,为了防止请求重试时连续的冲突,我们需要一个时间间隔乘数,这里我设置了间隔乘数为 1.2 ,表示第一次重试间隔时间为 1 s,第二次间隔时间为 1.2 秒,第三次间隔时间为 1.44 秒…
- max-interval 表示重试的最大间隔时间
开启了请求重试机制之后,即使在弱网环境下,我们也能有效保证服务的可用性。