大纲 前言 版本说明 在下面的的教程中,使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,特别声明除外。
Config 使用技巧 本地参数覆盖远程参数 在某些时候需要使用当前系统的环境变量或者是应用本身设置的参数而不是使用远程拉取的参数,此时 Config Client 可以使用如下配置:
官方 Bug 解决方案:
1 2 3 4 5 6 spring: cloud: config: overrideNone: true allowOverride: true overrideSystemProperties: false
overrideNone:当 allowOverride 为 true 时,overrideNone 设置为 true,代表外部配置的优先级更低,而且不能覆盖任何已存在的属性源,默认为 false allowOverride:标识 overrideSystemProperties 属性是否启用,默认为 true,设置为 false 表示禁止用户的个性化设置 overrideSystemProperties:用来标识外部配置是否能够覆盖系统属性,默认为 true 服务端 Git 配置详解 Git 中 URI 占位符 Spring Cloud Config Server 支持占位符的使用,支持 {application}
、{profile}
、{label}
,这样的话就可以在配置 uri 的时候,通过占位符使用应用名称来区分应用对应的仓库然后进行使用。下面举例说明 {application}
占位符的使用,点击下载 完整的案例代码。
Config Server 的 application.yml
配置文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 9090 spring: application: name: config-server cloud: config: server: git: uri: https://gitee.com/peter/{application} username: admin password: admin search-paths: book-config
Config Client 的 bootstrap.yml
配置文件如下:
1 2 3 4 5 6 7 spring: cloud: config: label: master profile: dev uri: http://localhost:9090 name: spring-cloud-config
使用上面的配置后,Config Client 请求 Config Server 仓库的连接地址的 uri 变成了 https://gitee.com/peter/spring-cloud-config
,连接到了 spring-cloud-config
仓库;其中仓库的名称是由 Config Client 的 spring.cloud.config.name
属性指定,请求的配置文件的完整路径是 https://gitee.com/peter/spring-cloud-config/book-config/spring-cloud-config.yml
;值得注意的是,这里需要仓库名称和仓库下面的配置文件名称一致才可以。
路径搜索占位符 Spring Cloud Config Server 可以使用 searchPaths
参数进行路径的搜索,支持根据路径和路径前缀等方式进行配置文件的获取。
下述配置中的 book-config
表示匹配当前路径下面所有的配置文件信息,book-config*
表示在以 book-config
为前缀的文件夹内搜索所有配置文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 9090 spring: application: name: config-server cloud: config: server: git: uri: https://gitee.com/peter/spring-cloud-config username: admin password: admin search-paths: book-config, book-config*
下述配置中使用占位符的形式进行目录搜索,这样就可以根据不同的项目,对不同的配置文件进行路径搜索,从而很好地划分配置文件。值得注意的是,这里占位符的前后需要加上单引号,否则占位符无法生效。
1 2 3 4 5 6 7 8 9 10 11 12 spring: application: name: config-server cloud: config: server: git: uri: https://gitee.com/peter/spring-cloud-config username: admin password: admin search-paths: '{application}'
模式匹配和多个存储库 在 application
和 profile
的使用上,Spring Cloud Config Server 还支持更复杂配置模式,可以使用通配符 {application}/{profile}
进行规则匹配,多个规则需要通过逗号分隔。以下配置中的 spring.cloud.config.server.uri
指明了默认的仓库地址,在使用 {application}/{profile}
匹配不上任何一个仓库时,会使用默认的仓库进行匹配来获取信息。对于 spring-cloud-config-simples
匹配的是 spring-cloud-config-simples/*
,需要注意的是其仅能匹配应用名称为 spring-cloud-config-simples
的所有 profile 配置;对于 local 的仓库将会匹配所有的应用名以 local 开头的 Profiles。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 spring: cloud: config: server: git: uri: https://gitee.com/peter/spring-cloud-config search-paths: SC-BOOK-CONFIG repos: simple: https://gitee.com/peter/simple special: pattern: special*/dev*,*special*/dev* uri: https://gitee.com/peter/spring-cloud-config-special local: pattern: local* uri: /Users/peter/all_test/spring-cloud-config
关系型数据库的配置中心的实现 1. 基于 MySQL 的配置概述 Spring Cloud Config Server 默认提供了 JDBC 的方式连接 MySQL 数据库,整体的流程如下图,点击下载 完整的案例代码。
2. 创建 Maven 父级 Pom 工程 在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.0.3.RELEASE</version > </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > </dependencies > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > Finchley.RELEASE</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement > <repositories > <repository > <id > spring-milestones</id > <name > Spring Milestones</name > <url > https://repo.spring.io/libs-milestone</url > <snapshots > <enabled > false</enabled > </snapshots > </repository > </repositories >
3. 创建 Config Server 工程 创建 Config Server 的 Maven 工程,配置工程里的 pom.xml
文件:
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-server</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency >
在 MySQL 中执行下述数据库脚本,创建对应的数据库和表,并插入对应的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 create database `spring- cloud- config` default character set utf8;use `spring- cloud- config`; CREATE TABLE `PROPERTIES` ( `ID` int (11 ) NOT NULL AUTO_INCREMENT, `KEY` TEXT DEFAULT NULL , `VALUE ` TEXT DEFAULT NULL , `APPLICATION` TEXT DEFAULT NULL , `PROFILE` TEXT DEFAULT NULL , `LABLE` TEXT DEFAULT NULL , PRIMARY KEY (`ID`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8; INSERT INTO `spring- cloud- config`.`PROPERTIES` (`ID`, `KEY`, `VALUE `, `APPLICATION`, `PROFILE`, `LABLE`) VALUES ('3' , 'cn.springcloud.config' , 'I am the mysql configuration file from dev environment.' , 'config-client' , 'dev' , 'master' );INSERT INTO `spring- cloud- config`.`PROPERTIES` (`ID`, `KEY`, `VALUE `, `APPLICATION`, `PROFILE`, `LABLE`) VALUES ('4' , 'cn.springcloud.config' , 'I am the mysql configuration file from test environment.' , 'config-client' , 'test' , 'master' );INSERT INTO `spring- cloud- config`.`PROPERTIES` (`ID`, `KEY`, `VALUE `, `APPLICATION`, `PROFILE`, `LABLE`) VALUES ('5' , 'cn.springcloud.config' , 'I am the mysql configuration file from prod environment.' , 'config-client' , 'prod' , 'master' );
添加 Config Server 需要的 application.yml
配置文件到工程中,其中 spring.cloud.config.server.jdbc.sql
是在调用时使用的 SQL,spring.profiles.active=jdbc
表示使用的激活方式是 JDBC,spring.cloud.refresh.refreshable=none
是用来解决 DataSource 循环依赖问题。若项目中需要激活其他 profile
,那么可以指定多个,例如 spring.profiles.active=jdbc,dev
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 server: port: 8001 spring: application: name: config-server cloud: config: server: jdbc: sql: SELECT `KEY`, `VALUE` FROM PROPERTIES WHERE application =? AND profile =? AND lable =? label: master refresh: refreshable: none profiles: active: jdbc datasource: url: jdbc:mysql://127.0.0.1:3306/spring-cloud-config?useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver logging: level: org.springframework.jdbc.core: DEBUG org.springframework.jdbc.core.StatementCreatorUtils: Trace
创建 Config Server 的主启动类,增加 @EnableConfigServer
注解:
1 2 3 4 5 6 7 8 @EnableConfigServer @SpringBootApplication public class ConfigServerApplication { public static void main (String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }
启动 Config Server 应用后,浏览器输入 http://127.0.0.1:8001/config-client/dev/master
访问 Config Server,接口返回的结果如下:
4. 创建 Config Client 工程 创建 Config Client 的 Maven 工程,配置工程里的 pom.xml
文件,需要引入 spring-cloud-config-client
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-client</artifactId > </dependency >
创建 Config Server 的主启动类:
1 2 3 4 5 6 7 @SpringBootApplication public class ConfigClientApplication { public static void main (String[] args) { SpringApplication.run(ConfigClientApplication.class, args); } }
为了更好地观察拉取到的 MySQL 上面的配置,这里需要创建一个 Controller 用于访问返回配置信息,同时还需要创建一个实体,用于注入远程配置上的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Component @ConfigurationProperties(prefix = "cn.springcloud") public class ConfigProperties { private String config; public String getConfig () { return config; } public void setConfig (String config) { this .config = config; } }
1 2 3 4 5 6 7 8 9 10 11 @RestController public class ConfigController { @Autowired public ConfigProperties configProperties; @GetMapping("/getConfigInfo") public String getConfigInfo () { return configProperties.getConfig(); } }
添加 Config Client 需要的 application.yml
配置文件到工程中:
1 2 3 4 5 6 server: port: 9090 spring: application: name: config-client
添加 Config Client 需要的 bootstrap.yml
配置文件到工程中:
1 2 3 4 5 6 7 spring: cloud: config: name: config-client profile: dev label: master uri: http://127.0.0.1:8001
5. 关于配置的刷新问题 手动刷新和配置自动刷新对于 DB 环境下是否同时支持呢?对于 DB 操作来说,在自动刷新方面,一般是做了界面化的配置和管理,当成功提交配置到 DB 后,会调用 Config Server 的 Spring Cloud Bus 刷新接口,这样就可以实现和 Git 的 WebHook — 样的提交绑定执行功能。
6. 测试结果 依次启动 config-server、config-client 应用 访问 http://127.0.0.1:9090/getConfigInfo
,接口会返回 I am the git configuration file from dev environment
,说明一切运行正常 非关系型数据库的配置中心的实现 基于 MongoDB 的配置概述 Spring Cloud Config Server 并没有提供 MongoDB 的存储方式,但是目前 Spring Cloud 已经收录了一个相关的孵化器。整体的流程如下图,由于篇幅有限,下面只给出 Config Server 工程的核心配置和代码,而 Config Client 工程与上面 MySQL 的示例基本上一样,这里不再累述。
Config Server 工程的配置 Config Server 工程的 pom.xml
文件,添加 MongoDB 的依赖支持:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-server</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-server-mongodb</artifactId > <version > 0.0.2.BUILD-SNAPSHOT</version > </dependency >
Config Server 的主启动类,添加注解 @EnableMongoConfigServer
:
1 2 3 4 5 6 7 8 @SpringBootApplication @EnableMongoConfigServer public class MongoDbConfigServerApplication { public static void main (String[] args) { SpringApplication.run(MongoDbConfigServerApplication.class, args); } }
Config Server 工程里的 application.yml
文件
1 2 3 4 5 6 7 8 9 server: port: 8001 spring: application: name: config-server data: mongodb: uri: mongodb://localhost/springcloud
MongoDB 中的数据:
1 2 3 4 5 6 7 8 9 10 11 { "label" : "master" , "profile" : "dev" , "source" : { "cn" : { "springcloud" : { "config" : "I am the mongdb configuration file from dev environment. I will edit." } } } }
Config 功能扩展 客户端回退 客户端的回退机制,可以处理网络中断的情况,或者配置服务因维护而关闭的场景。当启用回退时,客户端适配器将 “缓存” 本地文件系统中的配置属性。要启用回退功能,只需指定存储缓存的位置即可;这个功能也称之为客户端高可用的一部分,也就是在服务端无法连接的情况下,客户端依然是可以用的,点击下载 完整的案例代码。
1. 准备工作 由于下面的 Spring Cloud Config 使用 Git 作为存储方式,因此需要提前在 Git 远程仓库(Github、Gitlab)中创建对应的仓库,然后往仓库里 Push 三个配置文件,分别是 config-client-dev.yml、config-client-prod.yml、config-client-test.yml,配置文件的内容如下:
1 2 3 4 5 6 server: port: 9001 cn: springcloud: config: I am the git configuration file from dev environment
1 2 3 4 5 6 server: port: 9002 cn: springcloud: config: I am the git configuration file from prod environment
1 2 3 4 5 6 server: port: 9003 cn: springcloud: config: I am the git configuration file from test environment
2. 创建 Config Client Fallback Autoconfig 工程 创建 Config Client Fallback Autoconfig 工程,配置工程里的 pom.xml
文件,其中 spring-security-rsa
依赖主要是用于当配置信息中存在敏感信息(如用户名密码)时,对敏感信息加密后再缓存在本地:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-config</artifactId > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-rsa</artifactId > </dependency >
创建 Config Client Fallback Autoconfig 工程里的 FallbackableConfigServicePropertySourceLocator 类,主要用来创建本地回退文件,也就是加载远程配置文件后在本地备份一份:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @Order(0) public class FallbackableConfigServicePropertySourceLocator extends ConfigServicePropertySourceLocator { private boolean fallbackEnabled; private String fallbackLocation; @Autowired(required = false) TextEncryptor textEncryptor; public FallbackableConfigServicePropertySourceLocator (ConfigClientProperties defaultProperties, String fallbackLocation) { super (defaultProperties); this .fallbackLocation = fallbackLocation; this .fallbackEnabled = !StringUtils.isEmpty(fallbackLocation); } @Override public PropertySource<?> locate(Environment environment) { PropertySource<?> propertySource = super .locate(environment); if (fallbackEnabled) { if (propertySource != null ) { storeLocally(propertySource); } } return propertySource; } private void storeLocally (PropertySource propertySource) { StringBuilder sb = new StringBuilder(); CompositePropertySource source = (CompositePropertySource) propertySource; for (String propertyName : source.getPropertyNames()) { Object value = source.getProperty(propertyName); if (textEncryptor != null ) value = "{cipher}" + textEncryptor.encrypt(String.valueOf(value)); sb.append(propertyName).append("=" ).append(value).append("\n" ); } System.out.println("file contents : " + sb.toString()); saveFile(sb.toString()); } private void saveFile (String contents) { BufferedWriter output = null ; File file = new File(fallbackLocation + File.separator + ConfigServerBootstrap.FALLBACK_FILE_NAME); try { if (!file.exists()) { file.createNewFile(); } output = new BufferedWriter(new FileWriter(file)); output.write(contents); } catch (IOException e) { e.printStackTrace(); } finally { if (output != null ) { try { output.close(); } catch (IOException e) { System.out.print("Error" + e.getMessage()); } } } } }
创建 Config Client Fallback Autoconfig 工程的自动配置类,添加相关注解,使其在 Spring Boot 启动的时候进行加载。其中 spring.cloud.config.fallbackLocation
是指回退配置文件所在的目录路径,file:${spring. cloud.config.fallbackLocation:}/fallback.properties
是指回退配置文件的完整路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration @EnableConfigurationProperties @PropertySource(value = {"config-client.properties", "file:${spring.cloud.config.fallbackLocation:}/fallback.properties"}, ignoreResourceNotFound = true) public class ConfigServerBootstrap { public static final String FALLBACK_FILE_NAME = "fallback.properties" ; @Autowired private ConfigurableEnvironment environment; @Value("${spring.cloud.config.fallbackLocation:}") private String fallbackLocation; @Bean public ConfigClientProperties configClientProperties () { ConfigClientProperties clientProperties = new ConfigClientProperties(this .environment); clientProperties.setEnabled(false ); return clientProperties; } @Bean public FallbackableConfigServicePropertySourceLocator fallbackableConfigServicePropertySourceLocator () { ConfigClientProperties client = configClientProperties(); FallbackableConfigServicePropertySourceLocator fallbackableConfigServicePropertySourceLocator = new FallbackableConfigServicePropertySourceLocator(client, fallbackLocation); return fallbackableConfigServicePropertySourceLocator; } }
创建 Config Client Fallback Autoconfig 工程里的 /src/main/resources/config-client.properties
配置文件:
1 spring.cloud.config.enabled=false
创建 Config Refresh Fallback Autoconfig 工程里 /src/main/resources/META-INF/spring.factories
配置文件,添加上面的自动配置类:
1 2 org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.springcloud.study.fallback.config.ConfigServerBootstrap
3. 创建 Config Client 工程 创建 Config Client 的 Maven 工程,配置工程里的 pom.xml
文件,需要引入上面的 config-client-fallback-autoconfig
:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-client</artifactId > </dependency > <dependency > <groupId > com.springcloud.study</groupId > <artifactId > config-client-fallback-autoconfig</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
创建 Config Client 的主启动类:
1 2 3 4 5 6 7 @SpringBootApplication public class ConfigClientApplication { public static void main (String[] args) { SpringApplication.run(ConfigClientApplication.class, args); } }
为了更好地观察拉取到的 Git 上面的配置,这里需要创建一个 Controller 用于访问返回配置信息,同时还需要创建一个实体,用于注入远程配置上的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Component @ConfigurationProperties(prefix = "cn.springcloud") public class ConfigProperties { private String config; public String getConfig () { return config; } public void setConfig (String config) { this .config = config; } }
1 2 3 4 5 6 7 8 9 10 11 @RestController public class ConfigController { @Autowired public ConfigProperties configProperties; @GetMapping("/getConfigInfo") public String getConfigInfo () { return configProperties.getConfig(); } }
添加 Config Client 需要的 application.yml
配置文件到工程中:
1 2 3 spring: application: name: config-client
添加 Config Client 需要的 bootstrap.yml
配置文件到工程中,fallbackLocation
指定了回退文件的路径:
1 2 3 4 5 6 7 8 spring: cloud: config: name: config-client profile: dev label: master uri: http://127.0.0.1:8001 fallbackLocation: /tmp/config/config-client-dev/
4. 创建 Config Server 工程 创建 Config Server 的 Maven 工程,配置工程里的 pom.xml
文件,需要引入 spring-cloud-config-server
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-server</artifactId > </dependency >
创建 Config Server 的主启动类,增加 @EnableConfigServer
注解:
1 2 3 4 5 6 7 8 @EnableConfigServer @SpringBootApplication public class ConfigServerApplication { public static void main (String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }
添加 Config Server 需要的 application.yml
配置文件到工程中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 8001 spring: application: name: config-server cloud: config: server: git: uri: git@github.com:xxxxx/spring-cloud-config-study-repo.git search-paths: spring-cloud-config-study-repo/ strictHostKeyChecking: false private_key_file: /root/.ssh/id_rsa.pub label: master
5. 测试结果 依次启动 config-server、config-client 应用 访问 http://127.0.0.1:9001/getConfigInfo
后观察返回的配置信息,与此同时查看是否在目录 /tmp/config/config-client-dev/
下成功创建了回退文件 fallback.properties
关闭 config-server、config-client 应用,然后单独启动 config-client 应用;观察在不启动 config-server 的情况下,config-client 应用是否能正常启动 若 config-client 应用单独启动成功,config-client 应用会先尝试去连接 config-server,当连接失败后,会加载本地的回退配置文件,此时控制台输出的日志信息如下: 1 2 3 4 c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://127.0.0.1:8001 c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://127.0.0.1:8001. Will be trying the next url if available c.c.c.ConfigServicePropertySourceLocator : Could not locate PropertySource: I/O error on GET request for "http://127.0.0.1:8001/config-client/dev/master": 拒绝连接; nested exception is java.net.ConnectException: 拒绝连接 c.s.study.ConfigClientApplication : No active profile set, falling back to default profiles: default
客户端的安全认证机制 JWT Spring Cloud Config 客户端支持使用 JWT 身份验证方法代替标准的基本身份验证,这种方式需要对服务端和客户端都要改造,点击下载 完整的案例代码,具体的验证步骤如下:
客户端向服务端负载授权的 RestController 发送请求,并且带上用户名和密码 服务端成功验证用户名和密码后,返回 Jwt Token 客户端加载服务端的配置信息,需要在 Header 中带上 Token 令牌进行认证 i. 准备工作 本示例用到上面的 “客户端回退” 示例中 Git 仓库里的配置文件,包括 config-client-dev.yml、config-client-prod.yml、config-client-test.yml。
ii. 创建 Config Client Jwt 工程 创建 Config Client Jwt 工程,配置工程里的 pom.xml
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-autoconfigure</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-config</artifactId > </dependency >
创建 Config Client Jwt 工程的自动配置类,@PostConstruct
注解是执行是在 Servlet 构造函数和 init()
方法执行之间,也就是说在容器启动过程中会创建一个 RestTemplate
对象,将用户名和密码发送到 Config Server 端进行认证;认证成功会返回 Token,如果认证过程中用户名或者是密码错误,则将返回一个 401 认证失败的错误码。其中 ${spring.cloud.config.usemame}
、 ${spring.cloud.config.password}
等参数是配置在客户端的,这里需要创建 ConfigServicePropertySourceLocator 这个 Bean 并且自定义一个 RestTemplate 对象需要带上 Token 信息,这就是代码中的 customRestTemplate
方法。还需要定义一个 ClientHttpRequestlnterceptor 接口的实现类,也就是代码中的 GenericRequestHeaderInterceptor 类,主要用于拦截发送到 Config Server 获取配置信息的请求,将 Token 信息添加到 HttpServletRequest 的 Headers 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 @Configuration @Order(Ordered.LOWEST_PRECEDENCE) public class ConfigClientBootstrapConfiguration { private static Log logger = LogFactory.getLog(ConfigClientBootstrapConfiguration.class); @Value("${spring.cloud.config.username}") private String jwtUserName; @Value("${spring.cloud.config.password}") private String jwtPassword; @Value("${spring.cloud.config.endpoint}") private String jwtEndpoint; private String jwtToken; @Autowired private ConfigurableEnvironment environment; @PostConstruct public void init () { RestTemplate restTemplate = new RestTemplate(); LoginRequest loginBackend = new LoginRequest(); loginBackend.setUsername(jwtUserName); loginBackend.setPassword(jwtPassword); String serviceUrl = jwtEndpoint; Token token; try { token = restTemplate.postForObject(serviceUrl, loginBackend, Token.class); if (token.getToken() == null ) { throw new Exception(); } setJwtToken(token.getToken()); } catch (Exception e) { e.printStackTrace(); } } public String getJwtToken () { return jwtToken; } public void setJwtToken (String jwtToken) { this .jwtToken = jwtToken; } @Bean public ConfigServicePropertySourceLocator configServicePropertySourceLocator (ConfigClientProperties configClientProperties) { ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(configClientProperties); configServicePropertySourceLocator.setRestTemplate(customRestTemplate()); return configServicePropertySourceLocator; } @Bean public ConfigClientProperties configClientProperties () { ConfigClientProperties clientProperties = new ConfigClientProperties(this .environment); clientProperties.setEnabled(false ); return clientProperties; } private RestTemplate customRestTemplate () { Map<String, String> headers = new HashMap<>(); headers.put("token" , "Bearer:" + jwtToken); SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setReadTimeout((60 * 1000 * 3 ) + 5000 ); RestTemplate template = new RestTemplate(requestFactory); if (!headers.isEmpty()) { template.setInterceptors( Arrays.<ClientHttpRequestInterceptor>asList(new GenericRequestHeaderInterceptor(headers))); } return template; } public static class GenericRequestHeaderInterceptor implements ClientHttpRequestInterceptor { private final Map<String, String> headers; public GenericRequestHeaderInterceptor (Map<String, String> headers) { this .headers = headers; } @Override public ClientHttpResponse intercept (HttpRequest httpRequest, byte [] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { headers.entrySet().stream().forEach(header -> { httpRequest.getHeaders().add(header.getKey(), header.getValue()); }); return clientHttpRequestExecution.execute(httpRequest, bytes); } } }
创建 Config Client Jwt 工程里的实体类,用于传递用户信息:
1 2 3 4 5 6 7 8 9 10 11 12 @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class LoginRequest implements Serializable { @JsonProperty private String username; @JsonProperty private String password; }
1 2 3 4 5 6 7 8 9 @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Token implements Serializable { @JsonProperty private String token; }
在 Config Client Jwt 工程里创建 /src/main/resources/META-INF/spring.factories
配置文件,添加上面的自动配置类:
1 2 org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.springcloud.study.config.ConfigClientBootstrapConfiguration
iii. 创建 Config Server 工程 创建 Config Server 工程,配置工程里的 pom.xml
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-server</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > <version > 2.7</version > </dependency >
创建 Config Server 工程里的 JwtAuthenticationRequest 实体类,用于传递用户名和密码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class JwtAuthenticationRequest implements Serializable { private String username; private String password; public JwtAuthenticationRequest () { super (); } public JwtAuthenticationRequest (String username, String password) { this .setUsername(username); this .setPassword(password); } }
创建 Config Server 工程里的 JwtAuthenticationResponse 实体类,用于返回 Token 信息:
1 2 3 4 5 6 7 8 9 10 public class JwtAuthenticationResponse implements Serializable { private final String token; public JwtAuthenticationResponse (String token) { this .token = token; } }
创建 Config Server 工程里的 JwtUser 实体类,用于返回 JWT 用户认证信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class JwtUser implements UserDetails { private final String username; private final String password; private final Collection<? extends GrantedAuthority> authorities; public JwtUser (String username, String password, Collection<? extends GrantedAuthority> authorities) { this .username = username; this .password = password; this .authorities = authorities; } @Override public String getUsername () { return username; } @JsonIgnore @Override public boolean isAccountNonExpired () { return true ; } @JsonIgnore @Override public boolean isAccountNonLocked () { return true ; } @JsonIgnore @Override public boolean isCredentialsNonExpired () { return true ; } @JsonIgnore @Override public String getPassword () { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isEnabled () { return true ; } @Override public String toString () { return "JwtUser [username=" + username + ", password=" + password + ", authorities=" + authorities + "]" ; } }
创建 Config Server 工程里的 JWT Token 认证过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class JwtAuthenticationTokenFilter extends UsernamePasswordAuthenticationFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; private final String tokenHeader = "token" ; @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String authToken = httpRequest.getHeader(tokenHeader); String username = jwtTokenUtil.getUsernameFromToken(authToken); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null ) { UserDetails userDetails = this .userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null , userDetails.getAuthorities()); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest)); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }
创建 Config Server 工程里的 JWT 工具类,主要用于根据传递过来的用户信息生成 JWT 的 Token,或者是验证请求的 Token 是否合法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 @Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -8652360919584431721L ; private static final String CLAIM_KEY_USERNAME = "sub" ; private static final String CLAIM_KEY_AUDIENCE = "audience" ; private static final String CLAIM_KEY_CREATED = "created" ; private static final String AUDIENCE_UNKNOWN = "unknown" ; private static final String AUDIENCE_WEB = "web" ; private Key secret = MacProvider.generateKey(); private Long expiration = (long ) 120 ; public String generateToken (JwtUser userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_AUDIENCE, AUDIENCE_WEB); claims.put(CLAIM_KEY_CREATED, new Date().getTime() / 1000 ); return generateToken(claims); } private String generateToken (Map<String, Object> claims) { return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret).compact(); } private Date generateExpirationDate () { return new Date(System.currentTimeMillis() + expiration * 1000 ); } public String getUsernameFromToken (String token) { if (token == null ) { return null ; } String username; try { final Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null ; } return username; } private Claims getClaimsFromToken (String token) { Claims claims; final String tokenClean = token.substring(7 ); try { claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(tokenClean).getBody(); } catch (Exception e) { claims = null ; } return claims; } public Boolean validateToken (String token, UserDetails userDetails) { JwtUser user = (JwtUser) userDetails; final String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token)); } private Boolean isTokenExpired (String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } public Date getExpirationDateFromToken (String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null ; } return expiration; } }
创建 Config Server 工程里的 JWT 认证端点类,主要用于在认证过程中,若认证未能通过直接返回 401 状态码:
1 2 3 4 5 6 7 8 9 @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint , Serializable { @Override public void commence (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" ); } }
创建 Config Server 工程里的账号验证类,主要用于客户端的验证用户名和密码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Service public class MemberServiceImpl implements UserDetailsService { private static final PasswordEncoder BCRYPT = new BCryptPasswordEncoder(); @Value("${spring.security.user.name}") private String hardcodedUser; @Value("${spring.security.user.password}") private String password; @Override public JwtUser loadUserByUsername (String username) throws UsernameNotFoundException { String hardcodedPassword = BCRYPT.encode(password); if (username.equals(hardcodedUser) == false ) { throw new UsernameNotFoundException(String.format("No user found with username '%s'." , username)); } else { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER" ); List<GrantedAuthority> grantedAuthorityList = new ArrayList<GrantedAuthority>(); grantedAuthorityList.add(simpleGrantedAuthority); return new JwtUser(hardcodedUser, hardcodedPassword, grantedAuthorityList); } } }
创建 Config Server 工程的 WebAuthenticationDetailsSourceImpl 类,用于将传递过来的对象数据封装到 JwtAuthenticationRequest 里面,该类负责将数据封装成 JSON 格式后返回给客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component public class WebAuthenticationDetailsSourceImpl implements AuthenticationDetailsSource <HttpServletRequest , JwtAuthenticationRequest > { @Override public JwtAuthenticationRequest buildDetails (HttpServletRequest request) { Gson gson = new Gson(); String json = new String(); String output = new String(); BufferedReader br; StringBuffer buffer = new StringBuffer(16384 ); JwtAuthenticationRequest jwtAuthenticationRequest = new JwtAuthenticationRequest(); try { br = new BufferedReader(new InputStreamReader(request.getInputStream())); while ((output = br.readLine()) != null ) { buffer.append(output); } json = buffer.toString(); jwtAuthenticationRequest = gson.fromJson(json, JwtAuthenticationRequest.class); } catch (IOException e) { e.printStackTrace(); } return jwtAuthenticationRequest; } }
创建 Config Server 工程的 AuthenticationRestController 类,主要用于颁发 Token 给客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @RestController public class AuthenticationRestController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private MemberServiceImpl userDetailsService; @Autowired private WebAuthenticationDetailsSourceImpl webAuthenticationDetailsSource; @RequestMapping(value = "/auth", method = RequestMethod.POST) public ResponseEntity<?> createAuthenticationToken(HttpServletRequest request) { JwtAuthenticationRequest jwtAuthenticationRequest = webAuthenticationDetailsSource.buildDetails(request); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(jwtAuthenticationRequest.getUsername(), jwtAuthenticationRequest.getPassword()); authToken.setDetails(jwtAuthenticationRequest); Authentication authenticate = authenticationManager.authenticate(authToken); SecurityContextHolder.getContext().setAuthentication(authenticate); JwtUser userDetails = userDetailsService.loadUserByUsername(jwtAuthenticationRequest.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } }
创建 Config Server 工程的 SecurityConfig 类,主要作用是进行安全认证和 Token 的过滤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationEntryPoint unAuthorizedHandler; @Autowired private WebAuthenticationDetailsSourceImpl webAuthenticationDetailsSource; @Bean @ConditionalOnMissingBean(AuthenticationManager.class) public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter (AuthenticationManager authenticationManager) throws Exception { UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter(); usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager); usernamePasswordAuthenticationFilter.setAuthenticationDetailsSource(webAuthenticationDetailsSource); return usernamePasswordAuthenticationFilter; } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); } @Bean public JwtAuthenticationTokenFilter authenticationTokenFilter () throws Exception { JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter = new JwtAuthenticationTokenFilter(); jwtAuthenticationTokenFilter.setAuthenticationManager(authenticationManager()); jwtAuthenticationTokenFilter.setAuthenticationDetailsSource(webAuthenticationDetailsSource); return jwtAuthenticationTokenFilter; } @Override protected void configure (HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .exceptionHandling().authenticationEntryPoint(unAuthorizedHandler) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/" ).permitAll() .antMatchers("/auth/**" ).permitAll() .anyRequest().authenticated().and().formLogin() .authenticationDetailsSource(webAuthenticationDetailsSource) .permitAll(); httpSecurity.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); } }
iiii. 创建 Config Client 工程 这里的 Config Client 工程与上面 “客户端回退” 示例中的 Config Client 工程的代码一致,直接拷贝一份即可,这里不再累述。
Config Client 里的 pom.xml
文件,引入上面的 config-client-jwt
依赖:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-config-client</artifactId > </dependency > <dependency > <groupId > com.springcloud.study</groupId > <artifactId > config-client-jwt</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
Config Client 里的 application.yml
配置文件:
1 2 3 spring: application: name: config-client
Config Client 里的 bootstrap.yml
配置文件,其中 password
和 username
是 Config Server 端配置需要的认证用户信息,endpoint
是一个 Config Server 访问验证授权的地址:
1 2 3 4 5 6 7 8 9 10 11 spring: cloud: config: name: config-client profile: dev label: master uri: http://127.0.0.1:8001 username: admin password: 123456 enabled: false endpoint: http://localhost:8001/auth
iiiii. 测试结果 依次启动 config-server、config-client 应用 访问 http://127.0.0.1:9090/getConfigInfo
,接口会返回 I am the git configuration file from dev environment
,说明一切运行正常 config-client 特意填写错误的账号信息,然后重新启动 config-client 应用,观察控制台是否会出现 401 授权失败的错误