终于把Apollo存储加密这件事搞定了

本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。

一些比较重要的配置信息,比如密码之类的敏感配置,我们希望将配置加密存储,保证安全性。Apollo框架本身没有提供数据加密的功能,如果想要实现数据加密的功能有两种方式,第一种是改Apollo的源码,增加加解密的逻辑,第二种比较简单,基于第三方的框架来对数据进行解密。

jasypt-spring-boot是一个基于Spring Boot开发的框架,可以将properties中加密的内容自动解密,在Apollo中也可以借助于jasypt-spring-boot这个框架来实现数据的加解密操作。

jasypt-spring-boot GitHub地址:https://github.com/ulisesbocchio/jasypt-spring-boot

将我们需要加密的配置通过jasypt-spring-boot提供的方法进行加密,然后将加密的内容配置在Apollo中,当项目启动的时候,jasypt-spring-boot会将Apollo加密的配置进行解密,从而让使用者获取到解密之后的内容。

创建一个新的Maven项目,加入Apollo和jasypt的依赖:


     
     
     
     
  1. com.ctrip.framework.apollo

  2. apollo-client

  3. 1.1.0

  4. com.github.ulisesbocchio

  5. jasypt-spring-boot-starter

  6. 1.16

加入下面的依赖信息:


     
     
     
     
  1. server.port=8081

  2. app.id=SampleApp

  3. apollo.meta=http://localhost:8080

  4. apollo.bootstrap.enabled=true

  5. apollo.bootstrap.namespaces=application

  6. jasypt.encryptor.password=yinjihaunkey

  • jasypt.encryptor.password:配置加密的Key

创建一个加密的工具类,用于加密配置:


     
     
     
     
  1. public class EncryptUtil {

  2. /**

  3. * 制表符、空格、换行符 PATTERN

  4. */

  5. private static Pattern BLANK_PATTERN = Pattern.compile("\\s*|\t|\r|\n");

  6. /**

  7. * 加密Key

  8. */

  9. private static String PASSWORD = "yinjihaunkey";

  10. /**

  11. * 加密算法

  12. */

  13. private static String ALGORITHM = "PBEWithMD5AndDES";

  14. public static Map<String, String> getEncryptedParams(String input) {

  15. //输出流

  16. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1024);

  17. PrintStream cacheStream = new PrintStream(byteArrayOutputStream);

  18. //更换数据输出位置

  19. System.setOut(cacheStream);

  20. //加密参数组装

  21. String[] args = {"input=" + input, "password=" + PASSWORD, "algorithm=" + ALGORITHM};

  22. JasyptPBEStringEncryptionCLI.main(args);

  23. //执行加密后的输出

  24. String message = byteArrayOutputStream.toString();

  25. String str = replaceBlank(message);

  26. int index = str.lastIndexOf("-");

  27. //返回加密后的数据

  28. Map<String, String> result = new HashMap<String, String>();

  29. result.put("input", str.substring(index + 1));

  30. result.put("password", PASSWORD);

  31. return result;

  32. }

  33. /**

  34. * 替换制表符、空格、换行符

  35. *

  36. * @param str

  37. * @return

  38. */

  39. private static String replaceBlank(String str) {

  40. String dest = "";

  41. if (!StringUtils.isEmpty(str)) {

  42. Matcher matcher = BLANK_PATTERN.matcher(str);

  43. dest = matcher.replaceAll("");

  44. }

  45. return dest;

  46. }

  47. public static void main(String[] args) {

  48. System.out.println(getEncryptedParams("hello"));

  49. }

  50. }

执行main方法,可以得到如下输出:


     
     
     
     
  1. {input=0JK4mrGjPUxkB4XuqEv2YQ==, password=yinjihaunkey}

input就是hello加密之后的内容,将input的值复制存储到Apollo中,存储的格式需要按照一定的规则才行:


     
     
     
     
  1. test.input = ENC(0JK4mrGjPUxkB4XuqEv2YQ==)

需要将加密的内容用ENC包起来,这样jasypt才会去解密这个值。

使用的地方可以直接根据名称注入配置,比如:


     
     
     
     
  1. @Value("${test.input}")

  2. private String input;

input的值就是解密之后的值,使用者不需要关心解密逻辑,jasypt框架在内部处理好了。

jasypt整合Apollo也是有一些不足的地方,目前我只发现了下面几个问题:

  • 在配置中心修改值后,项目中的值不会刷新

  • 注入Config对象获取的值无法解密


     
     
     
     
  1. @ApolloConfig

  2. private Config config;

  3. @GetMapping("/config/getUserName3")

  4. public String getUserName3() {

  5. return config.getProperty("test.input", "yinjihuan");

  6. }

上面列举的2个问题,跟jasypt的实现方式是有关系的,意味着这种加密的方式可能只适合数据库密码之类的,启动时是可以解密的,而且只是用一次,如果是某些比较核心的业务配置需要加密的话,jasypt是支持不了的,无法做到实时更新。下章节我会讲解如何修改Apollo的源码来解决这2个问题。

扩展Apollo支持存储加解密

前面章节中给大家介绍了如何使用jasypt为Apollo中的配置进行加解密操作,基本的需求是能够实现的,但还是有一些不足的地方。

jasypt只是在启动的时候将Spring中带有ENC(xx)这种格式的配置进行解密,当配置发生修改时无法更新。由于Apollo框架本身没有这种对配置加解密的功能,如果我们想实现加解密,并且能够动态的更新,就需要对Apollo的源码做一些修改来满足需求。

对源码修改还需要重新打包,笔者在这边介绍一个比较简单的实现方式,就是创建一个跟Apollo框架中一模一样的类名进行覆盖,这样也不用替换已经在使用的客户端。

如果配置中心存储的内容是加密的,意味着Apollo客户端从配置中心拉取下来的配置也是加密之后的,我们需要在配置拉取下来之后就对配置进行解密,然后再走后面的流程,比如绑定到Spring中。在这个业务点进行切入之后,配置中心加密的内容就可以自动变成解密后的明文,对使用者透明。

通过分析Apollo的源码,笔者找到了一个最合适的切入点来做这件事情,这个类就是com.ctrip.framework.apollo.internals.DefaultConfig,DefaultConfig是Coonfig接口的实现类,配置的初始化和获取都会经过DefaultConfig的处理。

在DefaultConfig内部有一个更新配置的方法updateConfig,可以在这个方法中对加密的数据进行解密处理:


     
     
     
     
  1. private void updateConfig(Properties newConfigProperties, ConfigSourceType sourceType) {

  2. Set<Object> keys = newConfigProperties.keySet();

  3. for (Object k : keys) {

  4. String key = k.toString();

  5. String value = newConfigProperties.getProperty(key);

  6. // 加密Value

  7. if (value.startsWith("ENC(") && value.endsWith(")")) {

  8. logger.debug("加密Value {}", value);

  9. // 解密然后重新赋值

  10. try {

  11. String decryptValue = AesEncryptUtils.aesDecrypt(value.substring(3, value.length()-1), DECRYPT_KEY);

  12. newConfigProperties.setProperty(key, decryptValue);

  13. } catch (Exception e) {

  14. logger.error("加密配置解密失败", e);

  15. }

  16. }

  17. }

  18. m_configProperties.set(newConfigProperties);

  19. m_sourceType = sourceType;

  20. }

这边使用了AES来解密,也就是说配置中心的加密内容也需要用相同的加密算法进行加密,至于格式的话还是用的ENC(xx)这种格式来标识这就是一个加密的配置内容。解密之后将解密的明文内容重新赋值到Properties 中,其他的流程不变。

创建一个加密测试类,加密配置内容,复制存储到Apollo中


     
     
     
     
  1. public class Test {

  2. public static void main(String[] args) {

  3. String msg = "hello yinjihaun";

  4. try {

  5. String encryptMsg = AesEncryptUtils.aesEncrypt(msg, "1111222233334444");

  6. System.out.println(encryptMsg);

  7. } catch (Exception e) {

  8. e.printStackTrace();

  9. }

  10. }

  11. }

输出内容如下:

Ke4LIPGOp3jCwbIHtmhmBA==

存储到Apollo中需要用ENC将加密内容包起来,如下:

test.input = ENC(Ke4LIPGOp3jCwbIHtmhmBA==)

还是用之前的代码进行测试,Config获取和Spring注入的方式如可以成功的获取到解密的数据,并且在配置中心修改后也能实时推送到客户端成功解密。

本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。

终于把Apollo存储加密这件事搞定了_第1张图片

你可能感兴趣的:(终于把Apollo存储加密这件事搞定了)