在上一篇文章实现了基本结构的配置管理服务端和客户端,同时讲解了一些配置的基本原理。现在总结一下它是如何运作起来的。其中主要包含下面几个要素。
- 远程Git仓库:用来存储配置文件,上一篇文章我存储了应用名为repo的多环境配置文件:repo-{profile}.properties。
- Config Server:这是一个分布式配置中心,即config-server-vFinchley.RC2工程,在该工程中指定了Git仓库的位置。
- 本地Git仓库:在Config Server的文件系统中,每次客户端请求获取配置信息时,Config Server从Git仓库获取最新配置到本地,然后在本地Git中读取并返回。当远程仓库无法获取时,直接将本地内容返回。
- ServiceA、ServiceB:具体的微服务应用它们指定了Config Server的地址,从而实现从外部化获取应用本身要用到的配置信息。这些应用在启动的时候,会向Config Server请求获取配置信息来进行加载。
客户端应用从配置管理中获取配置信息遵从下面的执行流程:
1.应用启动时,根据bootstrap.yml(或者bootstrap.properties)中配置的应用名{application}、环境名{profile}、分支名{label},向Config Server请求获取配置信息。
2.Config Server根据自己维护的Git仓库信息和客户端传递过来的配置定位信息去查找配置信息。
3.通过 git clone命令将找到的配置信息下载到Config Server的文件系统中。
4.Config Server创建Spring的ApplicationContext实例,并从Git本地仓库中加载配置文件,最后将这些配置内容读取出来返回给客户端应用。
5.客户端应用在获得外部配置文件后加载到客户端的ApplicationContext实例,该配置内容的优先级高于客户端Jar包内部的配置内容,所以Jar包中重复的内容将不再被加载。
Config Server巧妙的通过git clone将配置信息存于本地,起到了缓存的作用,即使当Git服务端无法访问的时候,依然可以去Config Server中的缓存内容进行使用。
Git配置仓库
在Spring Cloud Config的服务端,对于配置仓库的默认实现采用了Git。Git非常适用于存储配置内容。它可以非常方便的使用各种第三方工具来对其内容进行管理更新和版本化,同时Git仓库的Hook功能还可以帮助我们实时的监控配置内容的修改。其中,Git自身的版本控制功能正是其他一些配置中心所欠缺的,通过Git进行存储意味着,一个应用的不同部署实例可以从Spring Cloud Config的服务端获取不同的版本配置,从而支持一些特殊的应用场景。
由于Spring Cloud Config默认使用Git,所以对Git的配置非常简单,只需要在Config Server的application.properties中设置spring.cloud.config.server.git.uri属性,为其指定Git仓库的网络地址和账户信息即可,就像我在《Spring Cloud系列(二十七) 分布式配置中心Spring Cloud Config—Finchley版本》中介绍的一样。
spring:
cloud:
config:
server:
git:
uri: https://github.com/WYA1993/spring_cloud_config_demo
search-paths:
- config/config_repo/
另外,Spring Cloud Config可以使用本地物理环境来代替Git远程仓库。
spring:
application:
name: config-server
profiles:
active:
- native
cloud:
config:
server:
native:
search-locations:
- file:D:\config\
另外还可以使用本地开发环境来代替Git远程仓库。
spring:
application:
name: config-server
profiles:
active:
- native
cloud:
config:
server:
native:
search-locations:
- classpath:/config
占位符配置URI
{application}、{profile}、{label}这些占位符除了用于标识配置文件的规则之外,还可以用于Config Server中对Git仓库地址的URI配置。现在我们可以通过{application}占位符来实现一个应用对应一个Git仓库地址的配置效果,具体实现如下:
spring:
cloud:
config:
server:
git:
uri: https://github.com/WYA1993/{application}
其中{application}是客户端的应用名,也就是spring.application.name属性的值,客户端发起请求后,Config Server会根据客户端的应用名来填充。比如我们从http://localhost:5666/spring_cloud_config_demo/dev/master去获取的话,spring_cloud_config_demo就是应用名,那么Config Server会将{application}自动占位为spring_cloud_config_demo,即去https://github.com/WYA1993/spring_cloud_config_demo这个仓库的根目录下找spring_cloud_config_demo-dev.properties资源。从而实现根据微服务应用的属性动态获取不同位置的配置。
上面的例子中可以看到{application}占位符的值是spring_cloud_config_demo,名字太长,你可以在Git中创建application-dev.properties来代替spring_cloud_config_demo-dev.properties文件,这样在请求http://localhost:5666/spring_cloud_config_demo/dev/master时Config Server会自动去匹配application-dev.properties文件。
另外,如果在{application}、{label}参数中包含了"/",那么在HTTP的URL中应该使用"(_)"来替代,以避免改变了URI的含义,指向错误的URI资源。例如:
spring:
cloud:
config:
server:
git:
uri: https://github.com/{application}
此时的HTTP请求为:http://localhost:5666/WYA1993(_)spring_cloud_config_demo/dev/master。
当我们使用Git作为配置中心来存储各个微服务应用配置文件的时候,该功能会变得非常有用,通过在URI中使用占位符可以帮助我们规划和实现通用的仓库配置。例如:
- 代码库:使用服务名作为Git仓库名称,比如会员服务的代码库https://github.com/WYA1993/member-service。
- 配置库:使用服务名加上-config后缀作为Git仓库名称,比如上面会员服务对应的配置库地址位置https://github.com/WYA1993/member-service-config。
这时我们就可以使用https://github.com/WYA1993/{application}-config配置,来同时匹配多个不同服务的配置仓库。
模式匹配和多个存储库
Spring Cloud Config还支持更复杂的需求,并在应用程序和配置文件名称上进行模式匹配。模式的格式是一组逗号分隔的{application}/{profile},其中的参数可以使用通配符,如以下示例所示:
spring:
cloud:
config:
server:
git:
uri: https://github.com/spring-cloud-samples/config-repo
repos:
simple: https://github.com/simple/config-repo
special:
pattern: special*/dev*,*special*/dev*
uri: https://github.com/special/config-repo
local:
pattern: local*
uri: file:/home/configsvc/config-repo
如果{application}/{profile}没有匹配到任何模式,它将使用默认的仓库地址:spring.cloud.config.server.git.uri。上面的例子中,"simple"仓库匹配的是“simple/*”(它仅仅匹配一个仓库simple,在所有的环境下)。"local"仓库将匹配所有{application}的名字以“local”开头的,并且也是在所有的环境下。“/*”前缀自动添加到所有没有设置{profile}的模式中。
每一个仓库也可以在子目录下存储配置文件,模式匹配也可以用于搜索这些目录,需要制定searchPaths,如下:
spring:
cloud:
config:
server:
git:
uri: https://github.com/spring-cloud-samples/config-repo
searchPaths: foo,bar*
上面的例子中,将在foo和以bar开头的目录中,搜索配置文件。
默认地,服务器在第一次请求配置文件时克隆远程的仓库,服务器也可以配置在启动的时候克隆仓库,如下:
spring:
cloud:
config:
server:
git:
uri: https://git/common/config-repo.git
repos:
team-a:
pattern: team-a-*
cloneOnStart: true
uri: http://git/team-a/config-repo.git
team-b:
pattern: team-b-*
cloneOnStart: false
uri: http://git/team-b/config-repo.git
team-c:
pattern: team-c-*
uri: http://git/team-a/config-repo.git
SVN配置仓库
Config Server除了支持Git外还支持SVN仓库,只需要做如下配置即可。
第一步,在pom.xml中引入SVN的依赖配置,让Config Server用于读取SVN内容的能力:
<dependency>
<groupId>org.tmatesoft.svnkit</groupId>
<artifactId>svnkit</artifactId>
<version>1.8.10</version>
</dependency>
第二步,在application.yml配置文件中使用SVN的配置属性来指定SVN服务器的位置以及访问的用户名与密码。
spring:
cloud:
config:
server:
svn:
uri: svn://localhost:443/myRepo/config-repo
username: username
password: password
本地仓库
在使用了Git或者SVN仓库之后,文件都会在Config Server的本地文件系统中存储一份,这些文件默认会使存储于以config-repo为前缀的临时目录中,比如名为/tmp/config/rep-<随机数>的目录。由于其随机性以及临时目录的特性,可能会有一些不可预知的后果,为了避免这个问题,最好的办法就是指定一个固定的位置来存储这些重要信息。只需要通过spring.cloud.config.server.git.basedir或spring.cloud.config.server.svn.basedir来配置一个我们准备好的目录即可。
本地文件系统
上面介绍到了两种使用本地文件系统的例子:本地物理环境和本地开发环境。这种方式不适用Git仓库或SVN仓库,直接使用本地文件系统的存储方式来保存配置信息。实现方式就是设置属性spring.active.profiles=active,Config Server会默认从应用的src/main/resource目录下搜索配置文件。如果需要指定搜索配置文件的路径,可以通过spring.cloud.config.server.native.search-locations来指定具体的配置文件。
日志输出
如果需要输出Config Server请求Git仓库的细节,可以设置以下包的日志级别为debug,这样通过日志跟踪可以理解Config Server的Git仓库配置,也方便定位问题。
logging:
level:
org.springframework.boot: debug
org.springframework.cloud: debug
健康监测
当使用占位符来配置URI的时候,很有可能在控制台中出现类似这样的警告信息(https://github.com/WYA1993/{application}-config为例):
o.s.c.c.s.e.JGitEnvironmentRepository: Could not fetch remote for master remote:
https://github.com/WYA1993/app-config
而引起该警告的原因是由于Spring Cloud Config 的服务端为actuator模块的/actuator/health端点实现了对应的健康检测器:
org.springframework.cloud.config.server.config.ConfigServerHealthIndicator。在该检测器中默认构建了一个application为app的仓库。而根据之前的配置规则{application}-config该检测器会不断的检查https://github.com/WYA1993/app-config仓库是否可以连通。此时我们访问配置中心的/actuator/health端点来查看它的健康状态:
从/actuator/health端点返回的信息可以看到,由于无法连通https://github.com/WYA1993/app-config仓库,使得配置中心的可用状态是DOWN。虽然我们依然可以通过URI的方式访问该配置中心,但是当配置中心服务化使用的时候,该状态将影响它的服务可用性判断。所以需要让它的健康监测器正常工作起来。
第一种方式:直接创建app-config仓库,让健康检测器能够访问到它。
第二种方式:配置一个已存在的仓库来进行连通检测,如下所示,它实现了通过连接check-repo-config仓库进行健康检测:
spring.cloud.config.server.git.uri=https://github.com/WYA1993/{application}-config
spring.cloud.config.server.health.repositories.check.name=check-repo
spring.cloud.config.server.health.repositories.check.label=master
spring.cloud.config.server.health.repositories.check.profiles=default
由于健康检测的repositories是一个Map对象,所以可以配置多个,每个配置都可以包含与定位仓库地址时类似的三个元素:
- name:应用名
- label:分支名
- profiles:环境名。
如果我们不想使用该健康检测器,也可以通过spring.cloud.config.server.health.enabled=false关闭它。
属性覆盖
Config Server有一个属性覆盖的特性,它可以让开发人员为所有应用提供配置属性,只需要通过spring.cloud.config.server.overrides属性来设置键值对的参数,这些参数会以Map的形式加载到客户端的配置中。
spring.cloud.config.server.overrides.name=hah
spring.cloud.config.server.overrides.from=shanghai
通过该属性配置的参数,不会被Spring Cloud的客户端修改,并且Spring Cloud客户端从Config Server中获取配置信息时,都会取得这些配置信息。利用该特性可以方便的为Spring Cloud应用配置一些共同属性或者默认属性。当然这些属性并非强制的,我们可以通过改变客户端中更高优先级的配置方式来选择是否使用Config Server提供的默认值。
安全保护
由于配置中心存储的内容比较敏感,做一定的安全处理是必须的。为配置中心实现安全保护的方式有很多,比如物理网络限制、OAuth2授权等。不过由于我们的微服务应用和配置中心都构建在Spring Boot基础上,所以与Spring Security结合更加方便。
只需要引入spring-boot-starter-security依赖不用做任何改动就能实现对配置中心访问的安全保护。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
默认情况下,我们会获得一个user用户,并且在配置中心启动时日志打印该用户的随机密码,具体如下:
2018-09-29 15:02:17.096 INFO 8764 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: b9f29920-16d6-415d-8bbd-8e802e2ebd26
大多数情况下,我们不会使用随机密码的机制,我们可以指定用户名和密码:
spring:
security:
user:
name: user
password: b9f29920-16d6-415d-8bbd-8e802e2ebd22
由于我们已经为config-server设置了安全保护,如果这时候配置中心的客户端中没有设置对应的安全信息,在启动客户端时就会报错导致启动失败。所以需要通过配置的方式进入安全信息来通过校验。
spring:
cloud:
config:
username: user
password: b9f29920-16d6-415d-8bbd-8e802e2ebd22
加密解密
如果我们直接将敏感信息以明文的方式存储在微服务应用的配置文件中是非常危险的。针对这个问题,Spring Cloud Config 提供了对属性进行加密解密的功能,以保护配置文件中的信息安全。
spring:
datasource:
username: dbuser
password: '{cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ'
在Spring Cloud Config中通过在属性值前使用{cipher}前缀来标注该内容是一个加密值,当微服务客户端加载配置时,配置中心会自动为带有{cipher}前缀的值进行解密。
使用前提
为了启用该功能,必须在配置中心的运行环境中安装不限长度的JCE版本。虽然JCE功能在JRE中自带,但是默认使用得是有长度限制的版本。我们可以从Oracle的官网下载它,它是一个压缩包,解压后可以看到三个文件。
我们需要将local_policy.jar和US_export_policy.jar两个文件复制到$JAVA_HOME/jre/lib/security目录下,覆盖原来的默认内容。到这里,准备工作就完成了。
相关端点
在完成了JCE的安装后,可以尝试启动配置中心。在控制台将会输出一些配置中心特有的端点:
/encrypt/status:查看加密功能状态的端点
/key:查看密钥的端点。
/encrypt:对请求的body进行加密的端点。
/decrypt:对请求的body进行解密的端点。
可以尝试访问/encrypt/status端点,返回内容:
{
"description": "No key was installed for encryption service",
"status": "NO_KEY"
}
这说明当前配置中心的加密功能还不能使用,因为没有为加密服务配置对应的密钥。
配置密钥
我们可以通过encrypt.key属性在配置文件中指定密钥信息(对称性密钥),比如:
encrypt.key=didispace
现在重启配置中心,再访问/encrypt/status端点,返回内容:
{
"status": "OK"
}
此时,我们配置中心的加密解密功能就已经可以使用了,不妨尝试访问一下/encrypt
和/decrypt
端点来进行加密和解密的功能。注意,这两个端点都是POST请求,加密和解密信息需要通过请求体来发送。比如,以curl
命令为例,我们可以通过下面的方式调用加密与解密端点:
$ curl localhost:7001/encrypt -d didispace
3c70a809bfa24ab88bcb5e1df51cb9e4dd4b8fec88301eb7a18177f1769c849ae9c9f29400c920480be2c99406ae28c7
$ curl localhost:7001/decrypt -d 3c70a809bfa24ab88bcb5e1df51cb9e4dd4b8fec88301eb7a18177f1769c849ae9c9f29400c920480be2c99406ae28c7
didispace
这里,我们通过配置encrypt.key
参数来指定密钥的实现方式采用了对称性加密。这种方式实现比较简单,只需要配置一个参数即可。另外,我们也可以使用环境变量ENCRYPT_KEY
来进行配置,让密钥信息外部化存储。
非对称加密
Spring Cloud Config的配置中心不仅可以使用对称性加密,也可以使用非对称性加密(比如:RSA密钥对)。虽然非对称性加密的密钥生成与配置相对复杂一些,但是它具有更高的安全性。下面,我们来具体介绍一下如何使用非对称加密。
首先,我们需要通过keytool
工具来生成密钥对。keytool
是JDK中的一个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数字签名)自我认证(用户向别的用户/服务认证自己)或数据完整性以及认证服务。在JDK 1.4以后的版本中都包含了这一工具,它的位置在:%JAVA_HOME%\bin\keytool.exe
。
生成密钥的具体命令如下:
$ keytool -genkeypair -alias config-server -keyalg RSA -keystore config-server.keystore
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: zhaiyongchao
您的组织单位名称是什么?
[Unknown]: company
您的组织名称是什么?
[Unknown]: organization
您所在的城市或区域名称是什么?
[Unknown]: city
您所在的省/市/自治区名称是什么?
[Unknown]: province
该单位的双字母国家/地区代码是什么?
[Unknown]: china
CN=zhaiyongchao, OU=company, O=organization, L=city, ST=province, C=china是否正确?
[否]: y
输入 <config-server> 的密钥口令
(如果和密钥库口令相同, 按回车):
再次输入新口令:
另外,如果我们不想逐步的输入那些提示信息,可以使用-dname
来直接指定,而密钥库口令与密钥口令可使用-storepass
和-keypass
来直接指定。所以,我们可以通过下面的命令直接创建出与上述命令一样的密钥库:
$ keytool -genkeypair -alias config-server -keyalg RSA \
-dname "CN=zhaiyongchao, OU=company, O=organization, L=city, ST=province, C=china" \
-keypass 222222 \
-keystore config-server.keystore \
-storepass 111111 \
默认情况下,上述命令创建的密钥只有90天有效期。如果我们想要调整它的有效期,可以通过增加-validity
参数来实现,比如我们可以通过下面的命令,让密钥的有效期延长到一年:
$ keytool -genkeypair -alias config-server -keyalg RSA \
-dname "CN=zhaiyongchao, OU=company, O=organization, L=city, ST=province, C=china" \
-keypass 222222 \
-keystore config-server.keystore \
-storepass 111111 \
-validity 365 \
上述的三种命令生成方式,最终都会在命令的当前执行目录下生成一个config-server.keystore
文件。下面,我们需要将它保存在配置中心的文件系统中的某个位置,比如放在当前的用户目录下,然后在配置中心中加入相关的配置信息:
encrypt.key-store.location=file://${user.home}/config-server.keystore
encrypt.key-store.alias=config-server
encrypt.key-store.password=111111
encrypt.key-store.secret=222222
如果我们将config-server.keystore
放在配置中心的src/main/resource
目录下,也可以直接这样配置:encrypt.key-store.location=config-server.keystore
。另外,非对称加密的配置信息也可以通过环境变量的方式进行配置,它们对应的具体变量名如下:
ENCRYPT_KEY_STORE_LOCATION
ENCRYPT_KEY_STORE_ALIAS
ENCRYPT_KEY_STORE_PASSWORD
ENCRYPT_KEY_STORE_SECRET
通过环境变量来配置密钥库相关信息可以获得更好的安全性,所以我们可以将敏感的口令信息存储在配置中心的环境变量中是一种不错的选择。