Spring Cloud配置服务
Keep Team Lv4

  这一段时间都在小云上折腾微服务的事儿,一边愉快的加着新功能,一边苦逼的调试着参数。也许这就是痛并快乐着吧。所幸Spring Cloud是一款优秀的服务框架。

它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。

  最大的特点就是代码与配置相互独立,开箱即用(写入极少代码),通过更改配置就可以调整功能细节,我这么努力吹Spring Cloud,社区不考虑请我吃饭吗?哈哈哈哈

恰饭饭

  虽然有如此优秀的框架,奈何本人学艺不精,技不如人,即便只加了极少的功能,也只能一遍又一遍的在云上不断的修改配置进行调试(为啥不在本地调好再上云呢?因为我买云就是为了随时随地想怎么玩就怎么玩呀😏)。可是为了验证一处小修改就重新编译一次,似乎又太重载了。一开始还只是一个微服务需要修改配置验证,觉得忍一忍也就算了,后来发展到需要多个微服务配合修改才能验证一个功能,就实在受不了了,于是乎决定上线配置服务器,一劳永逸的解决这个痛点。

yahoo

优势

  上述场景就是配置服务一个典型应用场景。通常软件开发的时候,首先会制作一个通用版本,以满足大部分场景使用,而落地到实际生产环境上又会有不同的配置参数(如:用户名、密码、超时时间等),这就促使开发人员将配置文件与功能逻辑相互独立保存(也就是常说的不要在代码中有硬编码行为)。

  配置服务就是一个配置文件管理服务,通过Rest接口对外提供配置查询功能,并且支持多种存储方式(如本地文件、Git仓库等)。基于Spring Cloud有很多优秀的配置服务,如:Spring Config、Spring Consul、携程Apollo、蚂蚁金服disconf 等,因此给大伙的可选项还是挺多的。

Spring Cloud Config

  Spring Cloud Config的历史挺长,也算是Could框架中最早的几个成员之一,虽然现在更新较慢(似乎很长时间未更新了),不过其提供的服务已能满足我这样小白用户的需求。其秉承Spring Boot的优良传统,同样支持开箱即用。

服务端配置

  既然Spring Cloud Config服务是基于Spring Boot的REST应用程序,因此我们可以将Spring Cloud配置服务嵌入在其它Spring Boot当中,当然也可以完全新建一个独立的Spring Cloud Config服务。

  创建方式很简单,只需要在POM中引入spring-cloud-config-server即可。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>

  在入口函数中添加上Spring Cloud注解(@EnableConfigServer),即告诉Spring Cloud启动配置服务。

  此时程序就拥有了提供配置服务的能力,不过还需要做更多的配置,以便告诉服务需要如何运行。相对来说从一个文件目录中检索配置信息是最简单的运行模式,而这个目录可以是本地目录,也可以是网络映射目录。

基于文件系统管理

  基于文件系统管理的配置服务,只需要告诉Spring Cloud Config在哪个或哪几个目录搜索配置文件即可,在配置文件中添加如下信息:
spring.cloud.config.server.native.searchLocations: file:///mnt/config-repo/ms-gateway

  file后面的路径需为服务可直接访问的目录,若需要多个则可用半角逗号(,)进行分隔。这句配置的意思是Spring Cloud Config服务以Native方式运行(文件系统),且在以下路径中(file:///mnt/config-repo/ms-gateway)匹配配置文件。

  • 优点

  基于文件系统的配置服务操作简单,不需要提供额外的服务即可支持。

  • 不足

  配置文件更新维护成本较高,这里的成本并不是指对配置文件本身的验证成本,而是文件系统维护的成本,如:如何确保文件系统的容错性(误删、误改等)。

基于Git管理

  针对文件系统的不足,业内早就有各种解决方案,比如版本管理工具(如Git以及 SVN等)。而Spring Cloud Config就可以支持Git仓库。

  • 优势

  对于文本类型的配置文件来说引入Git优势就太多了,可以跟踪修改记录、可以标记tag、可以分支管理、可以回退更改等等。而这一切带来的结果就是维护成本降低了。

  • 不足

  非要说有啥不足,可能是需要git仓库以及懂得基本的git操作吧,否则还真没发现有啥不足的,有钱的站长可以自建gitlab私有仓库,像我这样没钱又爱瞎折腾的则可以选择github或者gitee做仓库。

  同样除了在POM中指定Config依赖之外,还需要在配置文件中添加以下内容:

spring.cloud.config.server.git.uri=$GIT_SERVER_URI 
spring.cloud.config.server.git.username=$GIT_USERNAME 
spring.cloud.config.server.git.password=$GIT_PASSWORD 
spring.cloud.config.server.git.search-paths=$GIT_SEARCH_PATH

  相较于基于文件系统的配置,基于Git的则多了用户名、密码以及Git地址的信息。而spring.cloud.config.server.git.search-paths和之前的一样,用逗号区分多个目录。

配置查询

  上文说过Spring Cloud Config是基于 Rest的应用程序,那么使用方式当然绕不开 Get/Post。可通过Get的方式从配置服务中查询某个配置文件内容,目前支持以下访问格式。

  1. /{name}-{profiles}.properties
  2. /{name}/{profiles}/{label:.}
  3. /{name}/{profiles:.[^-].}
  4. /{name}/{profiles}/{label:.}
  5. /{label}/{name}-{profiles}.properties
  6. /{name}-{profiles}.json
  7. /{label}/{name}-{profiles}.json
  8. /{label}/{name}-{profiles}.yaml
  9. /{label}/{name}-{profiles}.yml
  10. /{name}/{profiles:.[^-].}
  11. /{name}-{profiles}.yaml
  12. /{name}-{profiles}.yml

  假设我们的配置服务器地址为https://localhost:8080,那么可以通过浏览器访问https://localhost:8080/ms-discovery/prod获取服务名为ms-discovery的配置信息。

example

  也可以直接访问文件名,如:https://localhost:8080/ms-discovery-prod.yml

example

  二者的区别在于是否返回文件的Git信息,在第一种方案当中能看到多了label、version等内容,其中version对应 Git commit id。

  上述查询的是prod属性配置文件,如果我们还有一个dev文件,则可以通过访问https://localhost:8080/ms-discovery/dev或https://localhost:8080/ms-discovery-dev.yml进行查询。此处的 prod 以及 dev 对应配置文件的profiles

  服务端的配置差不多就这样了吧,现在再来看看客户端的配置。

客户端配置

  和服务端一样首先在POM中加入配置服务的依赖。

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

  其次在配置文件中指明配置服务器的相关信息。同样以服务名为ms-discovery的程序为例,其配置如下:

spring:
  application:
    name: ms-discovery
  config:
    import: optional:configserver:${server.config.uri}
  cloud:
    config:
      name: ${spring.application.name}

  配置信息告诉应用程序三件事儿,第一件我是谁?、第二件配置在哪里?、第三件配置文件叫什么?,这里我将配置服务器地址以变量的方式传入(确保即便服务器地址发生变化,程序依然可访问到正确的地址)。其实除了配置服务器地址是通过变量传递的,程序运行时还有其他参数也是通过变量控制。完整运行命令如下:

java $JAVA_OPTIONS \
     -Dserver.tomcat.max-threads=$MAX_THREADS \
     -Dserver.config.uri=$CONFIG_URI \
     -Dspring.profiles.active=$PROFILE \
     -Dserver.port=$PORT \
     -Ddiscovery.server=$DISCOVERY_HOST \
     -Dtrace=$DEBUG \
     -jar /app.jar

  其中$JAVA_OPTIONS$MAX_THREADS均为优化选项,用于调整程序运行时性能参数(以后有机会再做介绍)。而$PROFILE$CONFIG_URI则是指明配置文件属性和服务器地址信息。在配置查询章节,我们已了解如何通过profiles参数去读取不同的配置内容。

  正如本文开篇所述,在折腾微服务的时候常常需要调整配置信息以适配新加的功能特性,而已经走到这里的我们具备了配置和代码分离管理的能力,修改配置后只需要重启微服务即可让配置生效。

  但是所谓“因为我懒,所以我要改变世界”,有没有什么办法不重启服务也让配置生效呢?那就得聊一聊配置刷新了。

配置刷新

  Spring Cloud Config通过添加注解@RefreshScope的方式,让应用的变量支持热更新(即通过访问应用的/refresh端点触发应用再次读取配置)。关于配置刷新网上有很多有深度且专业的代码解读,这里就不重复了。不过关于配置刷新我有另一种观点,在容器化流行的今天,也许重启一次容器会比热刷新配置效果更好。如本小站的每一个服务均运行在独立的Docker环境中,配合Docker-compose编排工具进行管理,如果有需要更新某个应用的配置,只需要执行docker-compose restart xxxx即可,既简单又高效。所以特性并没有谁更厉害,只有适合才是最重要的。

敏感信息保护

  在应用服务使用过程中不可避免会用到账号、密码这类敏感信息,如果放在配置文件中交给配置服务统一管理,又会担心信息的泄露(比如服务被攻击或者git仓库被破解),为了降低此类问题发生时带来的风险, Spring Cloud Config提供了一个重要的功能,即信息加密。

加/解密

  我们在配置文件中以密文的形式写下敏感信息,并告诉配置服务这是密文请解密后使用。密文格式为{cipher}密文

准备

  首先需要将执行环境中的JCE策略文件替换为无限制版本。

  1. 什么是JCE?

    JCE全称:Java Cryptography Extension,
    默认的内置JCE策略文件是一个受限版本,主要体现在加/解密的长度受限。因此我们需要用官网上下载的非受限版本替换本地的对应文件。
  2. 为什么受限?

    这不是美国佬的祖传操作吗?什么技术无国界,都是骗鬼的
  3. 怎么替换?

    下载好之后替换至/usr/lib/jvm/default-jvm/jre/lib/security/policy/unlimited(实际目录以系统为准)即可。

  然后配置一个只有你知道的密码,并将密码保存在执行机的环境变量当中。

export ENCRYPT_KEY=这里是切勿泄露的密码哟

  请按照执行机实际运行的系统设置环境变量。以环境变量执行的好处在于不用硬编码,特别是在以docker方式运行时,可随时变更密码。

加密

  将明文以text/plain类型,POST到/encrypt端点即可。得到的返回值就是密文。

  即便是同一个明文,每次加密的结果都是不同的。这样可以有效防止彩虹表攻击。

解密

  将密文以text/plain类型,POST到/decrypt端点即可。得到的返回值就是明文。

  本来可以通过postman体验的,不过我都折腾到这个份上了,那就整一个web界面呗,随时随地想玩就玩(快乐有时候就这么简单)。

example

  配置好之后再次通过配置服务访问配置文件就能看到密文被自动解密了。不过问题也来了,有时候我们并不想将解密后的明文就这样暴露在Rest接口返回值当中。那么我们可以告诉服务端,不要在服务端解密。而是由相关的应用程序自己对加密内容解密。

禁止解密

  在配置文件中设置spring.cloud.config.server.encrypt.enabled: false即可禁止服务端解密。同时在应用程序的执行环境中同样加入密码的环境变量。此时我们在访问配置查询端点,会发现密文未被解密。

decrypt

  这样网络传输的时候就不用担心信息泄露了。

  有了web界面固然用着很爽,但是万一被好心人访问到这个端点,岂不是所有密文都可以被破解了?看来还是得做一个鉴权呢。

资源服务器-权限管控

  本文一开篇就提到Spring Cloud Config可加入任何Spring Cloud应用程序当中,那么自然也可以作为 Resource Server的一个功能组件。要使用Resource Server需要在 POM中加入相关依赖。

<dependency>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

  其次添加相应的权限要求信息,以便在访问到相应端点的时候,能被filter拦截并做出对应的判断。文本不打算介绍 Resource Server,留在以后单独分享吧。

番外篇-基于数据库的配置管理

  Spring拥有很棒的数据库操作能力,我们通过@Entity以及@Repository等注解可以很方便的生成Spring Data。而Spring Data提供了基本的数据库CRUD能力。

  如果我们将配置信息保存在数据库当中,并且利用Spring Data和JPA框架,即可实现配置信息自动从数据库中读取并导入至应用程序当中,从而实现基于数据库的配置管理。

  这样做有一个小问题,无法原生支持/refresh的配置刷新。不过在某些场景下,此方案也许也有用武之地吧。

本文涉及的源码在Gitee