Spring Boot

最近在做项目的过程中,PSS提出配置文件中类似数据库连接需要的用户名、密码等敏感信息需要加密处理(之前一直是明文的)。

为了快速完成任务,网上搜刮到jasypt包,也有相应的starter,使用方法可以参考blog

但是还是想具体弄清楚背后的实现。偶然看到Spring Boot中有个EnvironmentPostProcessor接口。看名字,它的实现类应该在配置文件加载完和Spring容器开始初始化之前起作用。这样的话,我们就可以实现该接口用来定制化配置信息,包括解密。

话不多说,show code,

 1 @Component
 2 public class DecryptAESConfigProcessor implements EnvironmentPostProcessor {
 3 
 4     private static short INDEX = 0;
 5     private static String ITEM_FORMAT = "spring.config.decrypt-items[%d]";
 6     private static Pattern PATTERN = Pattern.compile("AES\((.+)\)");
 7     private static StringBuffer SB = new StringBuffer();
 8 
 9     @Override
10     public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
11         MutablePropertySources propertySources = environment.getPropertySources();
12         for (PropertySource propertySource: propertySources) {
13             if (propertySource instanceof OriginTrackedMapPropertySource){
14                 INDEX = 0;
15                 OriginTrackedMapPropertySource otmps = (OriginTrackedMapPropertySource)propertySource;
16                 //System.out.println("property name = " + otmps.getName());
17                 Map<String, Object> source = otmps.getSource();
18                 String secretSalt = source.getOrDefault("spring.config.secret-salt", "").toString();
19                 if (!"".equals(secretSalt)){
20                     String salt = CommonUtil.decrypt(secretSalt, "sns");
21                     while (INDEX > -1){
22                         String item = String.format(ITEM_FORMAT, INDEX);
23                         if (source.containsKey(item)){
24                             String itemValue = source.get(item).toString();
25                             String propertyValue = source.getOrDefault(itemValue, "").toString();
26                             Matcher matcher = PATTERN.matcher(propertyValue);
27                             boolean findAES = false;
28                             while (matcher.find()){
29                                 //decrypt each AES()
30                                 findAES = true;
31                                 String decryptStr = CommonUtil.decrypt(matcher.group(1), salt);
32                                 matcher.appendReplacement(SB, decryptStr);
33                             }
34                             if (!findAES){
35                                 //decrypt entire item
36                                 source.put(itemValue, CommonUtil.decrypt(propertyValue, salt));
37                             } else {
38                                 matcher.appendTail(SB);
39                                 source.put(itemValue, SB.toString());
40                             }
41                             SB.delete(0, SB.length());
42                             INDEX++;
43                         } else {
44                             INDEX = -1;
45                         }
46                     }
47                 }
48             }
49         }
50 
51     }
52 }

注意:需要将DecryptAESConfigProcessor声明到spring.factories中。

org.springframework.boot.env.EnvironmentPostProcessor=
org.chris.springboot.config_encrypt.config.DecryptAESConfigProcessor
  Spring Boot
public class CommonUtil {

    private static final String KEY_ALGORITHM = "AES";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
    private static Cipher CIPHER;
    private static KeyGenerator KEY_GENERATOR;

    static {
        try {
            CIPHER = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            KEY_GENERATOR = KeyGenerator.getInstance(KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        }
    }


    /**
     * Decrypt by AES
     * @param content
     * @param salt
     * @return
     */
    public static String decrypt(String content, String salt) {
        if (Objects.nonNull(content)) {
            try {
                byte[] decrypted = Base64.getDecoder().decode(content.getBytes("UTF-8"));
                CIPHER.init(Cipher.DECRYPT_MODE, getSecretKey(salt));
                return new String(CIPHER.doFinal(decrypted));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * Encrypt by AES
     * @param content
     * @param salt
     * @return
     */
    public static String encrypt(String content, String salt) {
        if (Objects.nonNull(content)) {
            try {
                CIPHER.init(Cipher.ENCRYPT_MODE, getSecretKey(salt));
                byte[] encrypted = CIPHER.doFinal(content.getBytes("UTF-8"));
                return Base64.getEncoder().encodeToString(encrypted);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * Generate encrypted salt
     * @param salt
     * @return
     */
    private static SecretKeySpec getSecretKey(final String salt) {
        KEY_GENERATOR.init(128, new SecureRandom(salt.getBytes()));
        SecretKey secretKey = KEY_GENERATOR.generateKey();
        return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
    }
}

最后,配置文件中需要有类似以下配置项

Spring Boot

Spring Boot

解密规则如下,

1. 采用AES加密解密;

2. 在配置文件中

 (1) 通过spring.config.decrypt-items指定需要解密的配置

 (2) 通过spring.config.secret-salt指定AES的key(最好加密)

3. 如果需要解密的配置项中存在AES()模式的字符串,将会解密 () 中的内容,否则解密整个配置项