image.png

前言

前几天,有个前同事向我吐槽,他们公司有个大神把公司的项目代码全部上传到了 github,并且是公开项目,所有人都可以浏览。更加恐怖的是项目里面包含配置文件,数据库信息、redis 配置、各种公钥私钥密码全在项目里面,也一同上传了。

如果只是单纯的业务代码泄露,情况倒还好,因为别人知道你代码,要想搞你,他必须要把源码看一遍,分析漏洞。 又因为代码上线,经过了层层测试,漏洞也不好找,至少短时间内不好找。但是别人拿到你的数据库信息,那就开启了上帝模式,想怎么玩就怎么玩,删库都不用跑路的。

不过,还好他们发现的及时,第一时间删除了 github 上的项目,但是不能保证当时的项目没有人拉到本地,所以第二就是把配置文件内的各项配置都更改一遍,改配置听起来简单,但是要知道有些配置是不能热更新的。很多配置要把前一个配置修改后才能使用,新老配置不能共存,你改的瞬间运行的项目就崩了,必须要停机维护才可以。为了变更配置他们花了大量的人力物力与精力。

其实这种惨痛的教训本可以避免的,防止配置泄露,通用的有两种形式。一种是使用配置中心,本地不保存配置,启动的时候从配置中心获取,这应该是最优解了。但是很多时候你所做的项目并没有使用配置中心,配置就在项目里面裸奔。这个时候就需要本地加密的形式防止配置泄露了,常用框架是 jasypt。同时它也是本文的主题,话不多说,直接开始,看看如果使用 jasypt 进行配置加密。

依赖

pom.xml

  1. <dependency>
  2. <groupId>com.github.ulisesbocchio</groupId>
  3. <artifactId>jasypt-spring-boot-starter</artifactId>
  4. <version>3.0.1</version>
  5. </dependency>
  6. <build>
  7. <plugins>
  8. <plugin>
  9. <groupId>com.github.ulisesbocchio</groupId>
  10. <artifactId>jasypt-maven-plugin</artifactId>
  11. <version>3.0.0</version>
  12. </plugin>
  13. </plugins>
  14. </build>

说明

如果你使用了 spring boot 那么使用 jasypt 很简单,只要依赖一个 jasypt-spring-boot-starter 包就可以了。

至于 jasypt-maven-plugin 是方便我们加密解密配置的 maven 插件,后面会说用法。

配置

application.properties

  1. my.conf.test1=123
  2. my.conf.test2=DEC(123)
  3. # 记得看最佳实践
  4. jasypt.encryptor.password=lE1rl5K$

说明

总共有三个配置,第一个配置 my.conf.test1 是不需要加密的配置,第二个配置 my.conf.test2 是需要加密的配置,要加密的内容是 123。注意他的格式的是 DEC(待加密内容)。第三个 jasypt.encryptor.password 配置是我们的加密私钥,默认使用的加密算法是 PBEWITHHMACSHA512ANDAES_256 ,这个密钥可以是任意字符串,而 lE1rl5K$ 只是我随机生成的,你可以自由发挥。

生成加密内容

好了,到目前为止,我们的配置还是明文的。my.conf.test2 是我们想加密的配置,他与 my.conf.test1 唯一的区别就是多了一个 DEC() 包裹,这算哪门子加密,其实我们还差一步。还记得我们上面加依赖的时候,配置了一个 Maven 插件吗?现在就是用到他的时候,在我们的项目目录路径下执行如下命令:

命令

  1. mvn jasypt:encrypt -Djasypt.encryptor.password="lE1rl5K$"

注意在执行的时候,password 要换成你自己在上文配置的密钥。执行完后,看到终端输出了一大堆日志,然后就没有然后了。但是真的是这样吗?

你再打开 application.properties 看一下,有什么不一样的地方。

  1. my.conf.test1=123
  2. my.conf.test2=ENC(0ZWzuD2DH0BZ8ANGMZxQyC6wv84sQLJtE6u7bcRjU+DntbMgkBvE2Z4fSzKKhYN8)
  3. jasypt.encryptor.password=lE1rl5K$

我们发现,三个配置中其它两个是原来的样子,但是 my.conf.test2 变了,首先格式从之前 DEC(xxx) 变成了 ENC(xxx) 。另外括号的 123 变成了 0ZWzuD2DH0BZ8ANGMZxQyC6wv84sQLJtE6u7bcRjU+DntbMgkBvE2Z4fSzKKhYN8

这其实就是配置加密后的样子。这条命令的功能其实很简单:

  1. 从配置文件中加载配置
  2. 从配置中找到有 DEC(xxx) 格式并且不是 jasypt 开头的配置
  3. 使用配置的密钥加密并覆盖配置为 ENC(加密后的值)

另外通过插件也可以解密,使用

  1. mvn jasypt:decrypt -Djasypt.encryptor.password="lE1rl5K$"

执行这条命令会反过来,把 ENC(xxx) 内容的配置解密成 DEC(明文) 打印在控制台,注意是控制台,而不是把配置文件变回去,作者说这样是为了安全。

验证

TestController.java

  1. /*
  2. *
  3. * * *
  4. * * * blog.coder4j.cn
  5. * * * Copyright (C) 2016-2019 All Rights Reserved.
  6. * *
  7. *
  8. */
  9. package cn.coder4j.study.example.jasypt;
  10. import org.springframework.beans.factory.annotation.Value;
  11. import org.springframework.web.bind.annotation.GetMapping;
  12. import org.springframework.web.bind.annotation.PathVariable;
  13. import org.springframework.web.bind.annotation.RequestMapping;
  14. import org.springframework.web.bind.annotation.ResponseBody;
  15. import org.springframework.web.bind.annotation.RestController;
  16. /**
  17. * @author buhao
  18. * @version TestController.java, v 0.1 2019-12-26 10:55 buhao
  19. */
  20. @RestController
  21. @RequestMapping("/test")
  22. public class TestController {
  23. @Value("${my.conf.test1}")
  24. private String confTest1;
  25. @Value("${my.conf.test2}")
  26. private String confTest2;
  27. @GetMapping("/getConf/{type}")
  28. @ResponseBody
  29. public Object getConfTest(@PathVariable Integer type) {
  30. if (type == 1) {
  31. return confTest1;
  32. } else {
  33. return confTest2;
  34. }
  35. }
  36. }

说明

代码其实很简单,首先通过 @Value 的方式读取配置,同时把没有加密的配置与加密的配置都读出来,然后通过接口,当路径参数为 1 的时候返回没有经过加密的参数,当路径参数为 2 的时候返回加密过的参数。要是都返回 123 说明我们成功了。

为了方便验证,直接用 IDEA 的内置工具,下面是验证结果:

未加密的参数

image.png

经过加密的参数

image.png

结果如我们所料,加密成功。

获取配置的大致流程其中跟上面加密配置的流程大致反过来:

  1. 拦截获取配置的操作
  2. 如果拦截到的配置是 ENC(xxx) 格式
  3. 读取 jasypt.encryptor.password 密钥
  4. 通过密钥解密配置

最佳实践

密钥与配置分开保存

可以看到,通过 jasypt 十分的方便,第一依赖,第二配置,其中配置除加密内容外还有一个 jasypt.encryptor.password 。这个前文也说了是用于加密与解密的密码,通过它可以加解密配置。

image.png

回到开头,我们加密的目的是为了防止代码泄露的时候把配置一起给泄露出去了。配置是没有问题了,我们加密了,但是我们同时把密钥也放在配置文件中了。这相当于什么呢?就像你把门给锁了,但是钥匙还插在锁上。

所以密钥一定要跟配置分开保存,通常是通过启动命令传给应用,比如下面这种:

  1. java -Djasypt.encryptor.password="password" -jar my-application.jar

如果再保险一点,可以把密钥放在环境变量中,再通过命令传给应用。

非对称加密

默认使用的加密算法为对称加密 HHMAC ,既然有对称那肯定也有非对称。

这里的对称与非对称指的是密钥的保存方式,对称加密是指的是加密与解密共用一个密钥,也就是说我用这个密钥即可以用来加密也可以用来解密。上一条说为了安全我们要把配置跟密钥分开保存,一般保存在两个地方,一个是线上服务器,一个是项目负责人的电脑上了,因为他要把配置从明文变成密文。为什么是项目负责人的电脑上,因为密钥不可能人手一份,那样又会增大泄露风险。

但是这样的话又会出来一种问题,一个项目涉及了太多配置,我加一个配置找下项目负责人帮我生成个密文,加一个生成一个,项目负责人变成工具人了。

这个时候我们可以通过非对称加密的方式来解决,这种方式的好处就是有一对密码,分别称为公钥与私钥,公钥用来生成加密数据,可以放心大胆人手一份,而私钥放在服务器上进行运行时候的解密工作,因篇幅有限,具体使用方式可以通过文末的链接查看官方文档。

环境隔离

配置肯定是区分环境的,有些环境安全等级没有那么高,比如开发与测试环境,没有必要加密。而预发及生产环境就需要加密,并且推荐使用不同的密钥,这样最大程度的避免安全问题。

其实相关

  1. jasypt-spring-boot (jasypt 的 github 地址,有着详尽的文档)
  2. 本文 DEMO (因为文章篇幅有限,只展示部分代码,具体代码已上传 github)