本文介绍的是配置信息的__动静态__加载,这里的动静态加载可以类比程序的编译时加载和运行时加载的概念,本文将举例说明静态加载配置的缺点、为了避免这些缺点引入动态加载配置的概念,并给出实际操作的方法。
静态加载配置 我们常常会将配置文件存放在项目上下文环境中,例如classpath下,或者项目某个目录下,启动时去读这个文件,这样的配置可以称之为静态加载配置。静态加载配置不够集中灵活,因为在项目打包的时候就决定了配置信息内容;最重要的是,这种配置方式不够安全,因为配置信息跟随代码,当我们将代码上传到github等开源的代码版本管理平台,那么一些敏感的配置(e.g. 数据库账号密码等)就暴露了。
拿jdbc的配置信息举例。
下面这行xml配置代码是使用jdbc配置数据库连接时在datasource.xml文件中的配置项。它包含一个location属性,该属性告诉了程序该去哪里加载配置信息,值中包含classpath说明这个配置存在于项目上下文中。
1 2 3 <context:property-placeholder location ="classpath:conf/db.properties" />
以下是上面location指明的db.properties文件中的内容,存储了数据库连接地址、用户名、密码,注意到这些都是敏感配置,很明显存在前面提到的静态加载配置的弊端:不安全,敏感信息暴露无遗。
1 2 3 4 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" , "" ); 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 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <bean id ="configurationCenterHolder" class ="framework.ConfigurationCenterHolder" > <property name ="locations" > <list > <value > classpath:conf/cc.properties</value > </list > </property > </bean > <bean id ="dataSource" class ="com.demo.DataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="${jdbc_url}" /> <property name ="username" value ="${jdbc_user}" /> <property name ="password" value ="${jdbc_password}" /> </bean >
1 2 3 4 jdbc_url =http\://localhost\:8110/jdbc project =javavirtual
使用第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 { String jdbcUrl = props.getProperty(JDBC_URL); String project = props.getProperty(PROJECT); String token = initCipher(project); String urlStr = jdbcUrl + "?token=" + URLEncoder.encode(token, "UTF-8" ) + "&project=" + project; String responseString = HttpClientUtil.request(urlStr); JSONObject json = JSONObject.parseObject(responseString); 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); } 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.cc项目应该和业务项目部署在同一个内网环境中,并且屏蔽外网对cc项目的访问。这样就完全隔绝的敏感配置信息与外网环境,就算业务项目代码泄漏也不会直接泄漏敏感配置信息。另外敏感信息以及cc接口认证的信息要配置在内网中配置了访问权限的文件中,这样保证了在内网中的只有对应的用户可以访问到对应的敏感文件。
2.这里提供的token认证只是一个示例,实际生产环境汇总可以给cc项目获取敏感配置信息的接口增加一些其他的安全措施来进一步确保安全(e.g. 接口签名)。
总结 至此,我们就从业务项目中剔除了敏感配置信息,并且可以从cc接口动态加载配置。另外,cc项目的代码中也不存在敏感信息,所以,业务项目和cc项目都是可以公开暴露的,我们只需要在确保内网不能访问cc的接口并且保证内网中存储的敏感配置有权限控制,这样的动态加载配置保证了敏感配置信息是私密的、安全的。
参考 应用敏感信息的 6 个配置原则