如何安全地加载敏感配置信息

本文介绍的是配置信息的__动静态__加载,这里的动静态加载可以类比程序的编译时加载和运行时加载的概念,本文将举例说明静态加载配置的缺点、为了避免这些缺点引入动态加载配置的概念,并给出实际操作的方法。

静态加载配置

我们常常会将配置文件存放在项目上下文环境中,例如classpath下,或者项目某个目录下,启动时去读这个文件,这样的配置可以称之为静态加载配置。静态加载配置不够集中灵活,因为在项目打包的时候就决定了配置信息内容;最重要的是,这种配置方式不够安全,因为配置信息跟随代码,当我们将代码上传到github等开源的代码版本管理平台,那么一些敏感的配置(e.g. 数据库账号密码等)就暴露了。

拿jdbc的配置信息举例。

下面这行xml配置代码是使用jdbc配置数据库连接时在datasource.xml文件中的配置项。它包含一个location属性,该属性告诉了程序该去哪里加载配置信息,值中包含classpath说明这个配置存在于项目上下文中。

1
2
3
<!-- datasource.xml -->
<!--可以将数据库连接池的配置文件单独放在另一个properties中-->
<context:property-placeholder location="classpath:conf/db.properties"/>

以下是上面location指明的db.properties文件中的内容,存储了数据库连接地址、用户名、密码,注意到这些都是敏感配置,很明显存在前面提到的静态加载配置的弊端:不安全,敏感信息暴露无遗。

1
2
3
4
# db.properties
jdbc_url=jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
jdbc_user=username
jdbc_password=password

动态加载配置

为了避免静态加载配置的缺点,我们很自然的想到需要干掉上面的db.properties以及datasource.xml中关于db.properties的配置信息。为了说明动态加载配置,我们将当前这个项目叫做业务项目。

基本思路是将db.properties中的信息放到一个独立于业务项目的地方存放。然后在业务项目启动加载datasource时动态的去这个独立的地方拿db.properties的信息并加载。这样就干掉了db.properties及datasource.xml中的敏感配置项,这就是本文要介绍的__动态加载配置__。上文提到的独立的地方可以是一个独立的项目,如了方便说明实际操作,这里我给这个项目取名为cc项目,取Configuration Center(配置中心)之意。

大致的示意图如下

下面就来看看具体如何操作

配置中心:cc项目

cc项目非常容易构建,如果只是为了独立数据库连接配置,那么可以只提供一个接口,在业务项目启动时,请求这个接口来获得数据库连接的敏感配置信息即可。

cc项目中提供敏感配置信息的接口

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
@RestController
@RequestMapping("/")
public class ConfigurationController {
//内网配置过访问权限的目录,该目录存放各业务项目的敏感配置
private static final String PREFIX = "/var/www/cc";

@RequestMapping("/jdbc")
public String getPropertiesByProject(Cgi cgi) throws IOException {
String project = cgi.getString("project", ""); //调用方业务项目名
String token = cgi.getString("token", ""); //接口调用凭证token
Properties properties = new Properties();
FileInputStream inputStream = new FileInputStream(PREFIX + "/" + project + ".properties");
properties.load(inputStream);
if (properties.isEmpty()) {
return "get properties error";
}
String savedCipher = properties.getProperty("cipher");
if (cipher.isEmpty() || savedCipher.isEmpty() || !cipher.equals(savedCipher)) {
return "authentication is failed";
}
String jdbc_url = properties.getProperty("jdbc_url");
String jdbc_user = properties.getProperty("jdbc_user");
String jdbc_password = properties.getProperty("jdbc_password");
JSONObject propertiesJson = new JSONObject();
propertiesJson.put("jdbc_url", jdbc_url);
propertiesJson.put("jdbc_user", jdbc_user);
propertiesJson.put("jdbc_password", jdbc_password);
System.out.println(propertiesJson.toString());
inputStream.close();

return propertiesJson.toString();
}
}
1
2
3
4
5
6
# ${project}.properties 这里的${project}根据调用的业务系统传入的project参数配置不同读取对应的properties文件
# 内网存储系统(配置过访问权限)中存储的某业务项目的敏感配置信息
token=2^$>[[.34337,8@4
jdbc_url=jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
jdbc_user=username
jdbc_password=password

业务项目加载敏感信息

业务项目获取敏感信息分两步:1.配置cc项目的接口及参数信息 2.启动时从cc接口加载敏感信息。

还是以加载jdbc数据源为例说明

  • 1.配置cc项目的接口及参数信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- datasource.xml 配置cc项目提供敏感配置的接口及所需参数信息 -->
<!-- 原加载配置的方式 <context:property-placeholder location="classpath:conf/db.properties"/>-->
<!-- 现在注入用于从cc项目拉取配置的类-->
<bean id="configurationCenterHolder"
class="framework.ConfigurationCenterHolder">
<property name="locations">
<list>
<value>classpath:conf/cc.properties</value><!-- 配置cc提供配置的接口信息 -->
</list>
</property>
</bean>

<bean id="dataSource" class="com.demo.DataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<!-- 基本属性 url、user、password,使用了形如${key}的占位符,从cc获取敏感配置后将替换成实际值 -->
<property name="url" value="${jdbc_url}"/>
<property name="username" value="${jdbc_user}"/>
<property name="password" value="${jdbc_password}"/>
</bean>
1
2
3
4
# cc.properties
# 这里的cc已经不存在可供外网访问的路由、用户名、密码等敏感信息,所以可以放心配置在项目中
jdbc_url=http\://localhost\:8110/jdbc
project=javavirtual
  • 2.启动时从cc接口加载敏感信息

使用第1步中配置的framework.ConfigurationCenterHolder类,该类继承了spring的PropertyPlaceholderConfigurer类,这是spring中的一个bean工厂后置处理器,重载其processProperties方法可以在程序运行时替换bean配置文件(xml)中用${key}占位的配置项。于是,我们可以结合cc的接口如下来实现动态加载敏感配置。

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
public class ConfigurationCenterHolder extends PropertyPlaceholderConfigurer {
private static final String JDBC_URL = "jdbc_url";
private static final String PROJECT = "project";

@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {
try {
//读取cc接口配置
String jdbcUrl = props.getProperty(JDBC_URL);
String project = props.getProperty(PROJECT);
String token = initCipher(project);
//请求cc接口获取敏感配置
String urlStr = jdbcUrl + "?token=" + URLEncoder.encode(token, "UTF-8") + "&project=" + project;
String responseString = HttpClientUtil.request(urlStr);
JSONObject json = JSONObject.parseObject(responseString);
//动态加载获取到的敏感配置信息,并填充占位符${key}
props.setProperty("jdbc_url", json.isEmpty() ? "" : (String) json.get("jdbc_url"));
props.setProperty("jdbc_user", json.isEmpty() ? "" : (String) json.get("jdbc_user"));
props.setProperty("jdbc_password", json.isEmpty() ? "" : (String) json.get("jdbc_password"));
LoggerUtil.info("init datasource success");

} catch (Exception e) {
LoggerUtil.error("init datasource error", e);
}
super.processProperties(beanFactoryToProcess, props);
}

//从内网存储系统(配置过访问权限)获取cc接口认证token参数
private String initCipher(String project) throws IOException {
String cipherFile = (SysConfig.DEV ? "/Users/." : "/home/.") + project;
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(cipherFile)), "utf-8"));
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0)
continue;

String token = line.trim().replaceAll("\\s+", " ");
return token;
}
reader.close();
return "";
}
}
1
2
3
# .${project} 与业务项目名对应的文件,用于存储调用cc接口的token
# 该token与cc接口读取的${project}.properties文件中的token一致方可通过cc接口认证,成功拿到敏感配置
2^$>[[.34337,8@4

注意

  • 1.cc项目应该和业务项目部署在同一个内网环境中,并且屏蔽外网对cc项目的访问。这样就完全隔绝的敏感配置信息与外网环境,就算业务项目代码泄漏也不会直接泄漏敏感配置信息。另外敏感信息以及cc接口认证的信息要配置在内网中配置了访问权限的文件中,这样保证了在内网中的只有对应的用户可以访问到对应的敏感文件。
  • 2.这里提供的token认证只是一个示例,实际生产环境汇总可以给cc项目获取敏感配置信息的接口增加一些其他的安全措施来进一步确保安全(e.g. 接口签名)。

总结

至此,我们就从业务项目中剔除了敏感配置信息,并且可以从cc接口动态加载配置。另外,cc项目的代码中也不存在敏感信息,所以,业务项目和cc项目都是可以公开暴露的,我们只需要在确保内网不能访问cc的接口并且保证内网中存储的敏感配置有权限控制,这样的动态加载配置保证了敏感配置信息是私密的、安全的。

参考

应用敏感信息的 6 个配置原则