实际项目开发过程中,我们的应用程序都有很多的配置文件(例如properties或者yml文件等),我们时常会遇到需要对配置文件敏感字段的参数内容进行加密处理(比如数据库连接密码、与第三方的通信密钥等)。
如果采用一定采用传统的springMVC做系统集成,我们可以继承PropertyPlaceholderConfigurer类并复写其converProperty方法,在该方法内一般需要做两步处理:
按照上面的思路,我们先实现自己的PropertyPlaceholderConfigurer子类,假如当前我们的需求是要将test.content参数值后面追加内容“《我是后加的内容》”
@Slf4j public class MyPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer { @Override protected String convertProperty(String propertyName, String propertyValue) { //这里做对属性的内容的加解密等操作 //TODO something if (propertyName.equalsIgnoreCase("test.content")) { log.info("当前即将过滤的内容[" + propertyName + "]=[" + propertyValue + "]"); propertyValue = propertyValue + "《我是后加的内容》"; } else if (propertyName.equalsIgnoreCase("spring.datasource.password")) { propertyValue = propertyValue.replace("abc", ""); } return super.convertProperty(propertyName, propertyValue); } }
接下来,我们要是实例化我们自定义的PropertyPlaceholderConfigurer,如果我们使用xml方式配置,则代码如下:
<bean id="propertyConfigurer" class="com.spring.boot.test.util.MyPropertPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:application.poperties</value> </list> </property> </bean>
如果采用javaconfig方式时代码如下:
@Bean public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer(){ PropertyPlaceholderConfigurer placeholderConfigurer=new MyPropertyPlaceholderConfigurer(); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource resource = resolver.getResource("classpath:application.properties"); placeholderConfigurer.setLocation(resource); return placeholderConfigurer; }
通过上面的步骤,我们可以在程序的TestService中使用自动注入或者Spring表达式@Value(“$(参数名)”)获取到的内容就是我们解密之后的内容。我们启动程序后我们可以看到日志内容
c.s.b.t.u.MyPropertyPlaceholderConfigurer[16] - 当前即将过滤的内容[test.content]=[测试内容] c.s.b.t.s.TestService[24] - 当前拿到的testContent=测试内容《我是后加的内容》
遗憾的时,上述方式存在两个缺点:
面对以上问题,直接通过修改PropertyPlaceholderConfigurer解决的路子我并没有去测试,不过接下来我们介绍另外一种方式解决这个问题,那就是spring boot集成jasypt框架实现对配置文件的参数内容加解密。
jasypt是一个java实现的安全框架,最新版本已经不提供PlaceholderConfigurer,所以我们很难直接使用该框架报对properties或者yml文件进行处理,但是国外大神Ulises Bocchio写了一个spring boot下用的工具包github地址 https://github.com/ulisesbocchio/jasypt-spring-boot ,该工具包支持使用jasypt框架来处理properties和yml配置文件参数内容的加解密操作,该工具已经发布到了中央仓库供大家使用。而且文档信息非常详细。下面我们简单说一下该工具的优势。
在早期的版本中jasypt为spring提供过EncryptablePropertyPlaceholderConfigurer与EncryptablePropertySourcesPlaceholderConfigurer两个类,但是后面的包中都不包含这两个类了。如果希望使用这种方式可以参考如下两篇博客:
最新的1.9.2版本中以上两个类都已经不存在了。所以推荐使用Ulises Bocchio写工具包来做系统集成。下面我们按照其文档简单配置一下使用思路。
首先,引入工具包依赖
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>1.14</version> </dependency>
如果不使用stater,我们可以引入如下包
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot</artifactId> <version>1.14</version> </dependency>
默认情况下jasypt采用的算法是PBEWithMD5AndDES,该算法对同一串明文每次加密的密文都不一样,比较适合做数据加解密。但是该算法必须配置密码,我们在yml文件配置如下参数
jasypt: encryptor: password: e!Jd&ljyJ^e4I5oU
如果想要改变其他配置例如密文的前后缀也可以在这里配置。
如果上面引入的包不是starter,那么需要在启动类上添加注解@EnableEncryptableProperties以启动该功能。
然后我们写一个junit测试类,具体内容如下
public class TestBootTest { @Autowired StringEncryptor stringEncryptor;//密码解码器自动注入 @Test public void test() { System.out.println(stringEncryptor.encrypt("123456")); } }
执行后的控制台输出123456加密后的内容:P7xVJnbrn/MCzyVEOejTRw==
小提示:在控制台下,我们直接使用jasypt工具包进行原始数据加密可以采用如下命令
java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=原文 password=密码 algorithm=加密方式
例如,我们要对中文“原文”进行加密,密码为123456,加密方式为PBEWithMD5AndDES,则具体命令为
java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=原文 password=123456 algorithm=PBEWithMD5AndDES
执行后会在控制台输出如下内容
----ENVIRONMENT----------------- Runtime: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 24.79-b02 ----ARGUMENTS------------------- algorithm: PBEWithMD5AndDES input: 原文 password: 123456 ----OUTPUT---------------------- BDhzghe2quZtDpX0ZM/e4w==
我们将spring.datasource.password的值配置为ENC(P7xVJnbrn/MCzyVEOejTRw==),为什么需要这么配置呢,因为默认情况下jasypt-spring-boot解析密码的规则是前缀是ENC(后缀是)。我们改造我们的单元测试工具类
public class TestBootTest { @Autowired StringEncryptor stringEncryptor;//密码解码器自动注入 @Value("${spring.datasource.password}") private String password; @Autowired private UserInfoMapper userInfoMapper; @Test public void test() { // System.out.println(stringEncryptor.encrypt("123456")); System.out.println("连接数据库密码:" + password); System.out.println("查询到的信息:"+userInfoMapper.selectByUserId(1)); } }
启动单元测试,可以看到日志输出了如下内容
连接数据库密码:123456 [2017-08-03 22:20:41,653][INFO ] c.z.h.HikariDataSource[95] - HikariPool-1 - Starting... [2017-08-03 22:20:42,200][INFO ] c.z.h.HikariDataSource[107] - HikariPool-1 - Start completed. 查询到的信息:UserInfo(id=1, userName=test1, userAge=20, address=四川成都, addTime=2016-06-05 19:32:42)
可以看到采用jasypt-spring-boot-starter工具来集成spring boot与jasypt的功能成功实现了。我们看再看下配置日志,可以看到默认情况下字符串加密机默认的配置信息
[2017-08-03 22:20:32,008][INFO ] c.u.j.StringEncryptorConfiguration[45] - Encryptor config not found for property jasypt.encryptor.algorithm, using default value: PBEWithMD5AndDES [2017-08-03 22:20:32,009][INFO ] c.u.j.StringEncryptorConfiguration[45] - Encryptor config not found for property jasypt.encryptor.keyObtentionIterations, using default value: 1000 [2017-08-03 22:20:32,009][INFO ] c.u.j.StringEncryptorConfiguration[45] - Encryptor config not found for property jasypt.encryptor.poolSize, using default value: 1 [2017-08-03 22:20:32,010][INFO ] c.u.j.StringEncryptorConfiguration[45] - Encryptor config not found for property jasypt.encryptor.providerName, using default value: null [2017-08-03 22:20:32,012][INFO ] c.u.j.StringEncryptorConfiguration[45] - Encryptor config not found for property jasypt.encryptor.saltGeneratorClassname, using default value: org.jasypt.salt.RandomSaltGenerator [2017-08-03 22:20:32,029][INFO ] c.u.j.StringEncryptorConfiguration[45] - Encryptor config not found for property jasypt.encryptor.stringOutputType, using default value: base64
通过以上方式使用jasypt框架处理配置文件参数加密的功能就基本上OK了,但是存在一定的风险,那就是程序配置文件中,存在解密密文的密码。因为PBEWithMD5AndDES算法到处都可以找到实现。如果拿到了数据库密文和算法的密码,那么很容易解析出连接数据库的密码。一般严谨的做法是不会将密文信息与解密工具放在一起,避免程序被获取后,加密算法和数据库密码密文以及解密密码都同时被泄露。我们公司就是采用C语言写了一个加解密工具,放在服务器上特定的位置。有Java程序中去调用该工具进行解密。那么这种情况下我们怎么自定义解密逻辑呢?
从单元测试中可以看到我们注入了一个加解密的接口StringEncryptor,跟踪改类的实现可以看到系统调用了encrypt方法用于明文加密,调用decrypt对密文进行解密。于是我们实现该接口,假如我们将数据库密码参数spring.datasource.password=123456配置为spring.datasource.password=ENC(654321),那么在该类里面我们要将654321替换为123456,得到我们的真实密码。代码如下
public class DefaultEncryptor implements StringEncryptor { @Override public String encrypt(String message) { if ("123456".equalsIgnoreCase(message)) { message = "654321"; } return message; } @Override public String decrypt(String encryptedMessage) { if ("654321".equalsIgnoreCase(encryptedMessage)) { log.info("将密文[654321]替换为[123456]"); encryptedMessage = "123456"; } return encryptedMessage; } }
接下来,我们使用java config方式实例化该对象替换默认的StringEncryptor实例,如下
@Bean(name = "stringEncryptor") public StringEncryptor stringEncryptor() { return new DefaultEncryptor(); }
我们运行单元测试,可以看到自定义密码解码器的日志信息
[2017-08-03 22:31:47,291][INFO ] c.s.b.t.u.DefaultEncryptor[22] - 将密文[654321]替换为[123456]
通过以上步骤,我们采用自定义的StringEncryptor实现了上下文中敏感信息的自定义加解密处理。
特别说明,文中引入的jasypt-spring-boot-starter的版本是1.14,该版本要求JRE版本必须为1.8。如果要想在1.6或者1.7下运行,那么必须使用1.4或1.5的版本的定制版本。
1.7版本下引入:
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>1.5-java7</version> </dependency>
1.6版本下引入:
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>1.5-java6</version> </dependency>
参考:
https://github.com/ulisesbocchio/jasypt-spring-boot