SpringBoot作为现在最为热门的后端框架,用的人非常多,不知道你们有没有注意到,绝大部分的人application.yml配置文件里的东西都是明文密码,这其实是有风险的,比如之前就有过某某公司不小心把代码开源了,然后配置文件的里数据库密码被别有用心的人利用,导致用户隐私泄露。 我最近也遇到同样的诉求,所以研究了一下怎么才能规避这样的风险。如果你Google“如何加密SpringBoot配置文件”类似的描述,绝大部分都会推荐你使用一个叫做jasypt的插件。说实话,这是一个很强大,而且简单好用的Java加解密工具库,但是我最终没有使用。 为啥没有使用?因为没有必要,自己撸更香! ## 原理剖析 当你使用了jasypt插件,会有一些配置需要你填写,其中一个必填项就是jasypt.encryptor.password,这是用来加解密的密码。对了,jasypt默认使用的是对称加密,如果你觉得安全性不够好,也有堆对称加密可以选。我觉得默认的就足够了。我们的目的就是防君子不防小人。 另外还有的配置是加密字段的前缀和后缀,这个默认为`ENC(`和`)`,中间的就是加密后的密文。 jasypt实现了BeanFactoryPostProcessor接口,重写了postProcessBeanFactory方法,在应用启动的时候,扫描所有配置项,对配置项中符合上述前缀后缀的值进行解密,并且替换。 ## 实战 现在你已经知道了原理,非常的简单,实现起来也没有什么难度。 首先我们需要写一个工具类,用于加密和解密信息。在类变量部分,我使用固定的算法、迭代次数和盐值长度,主要是方便使用的时候少传些参数,其实完全可以作为可选参数,来实现更复杂的加密。 ### EncryptUtil ```java /** * 信息加密工具类,使用PBEWithMD5AndDES加密算法,可对有一定安全性要求的信息进行加密。 * 安全性要求较高的信息请勿使用此工具进行加密。 * * @author xueye */ public final class EncryptUtil { private EncryptUtil() { throw new AssertionError("No com.cicdi.utils.EncryptUtil instances for you!"); } /** * 随机字符生成 */ private static final SecureRandom RANDOM = new SecureRandom(); /** * 使用的加密算法 */ private static final String ALGORITHM = "PBEWithMD5AndDES"; /** * 迭代次数 */ private static final int ITERATIONS = 1000; /** * 盐值长度 */ private static final int SALT_LENGTH = 8; /** * 使用PBEWithMD5AndDES算法对信息进行加密 * * @param message 需要加密的信息 * @return 加密后的信息 */ public static String encrypt(String message, String password) { try { return Base64.getEncoder().encodeToString(encrypt(message.getBytes(StandardCharsets.UTF_8), password)); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 对使用PBEWithMD5AndDES算法加密后的信息进行解密 * * @param encryptedMessage 经过PBEWithMD5AndDES算法加密后的信息 * @return 解密后的信息 */ public static String decrypt(String encryptedMessage, String password) { try { return new String(decrypt(Base64.getDecoder().decode(encryptedMessage), password), StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null; } private static byte[] encrypt(byte[] message, String password) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IOException, BadPaddingException, IllegalBlockSizeException { // 创建Key final SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); byte[] salt = generateSalt(SALT_LENGTH); final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS); SecretKey key = factory.generateSecret(keySpec); // 构建Cipher. final Cipher cipherEncrypt = Cipher.getInstance(ALGORITHM); cipherEncrypt.init(Cipher.ENCRYPT_MODE, key); // 保存参数 byte[] params = cipherEncrypt.getParameters().getEncoded(); // 加密信息 byte[] encryptedMessage = cipherEncrypt.doFinal(message); return ByteBuffer .allocate(1 + params.length + encryptedMessage.length) .put((byte) params.length) .put(params) .put(encryptedMessage) .array(); } private static byte[] decrypt(byte[] encryptedMessage, String password) throws BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException, IOException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException { int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]); int messageLength = encryptedMessage.length - paramsLength - 1; byte[] params = new byte[paramsLength]; byte[] message = new byte[messageLength]; System.arraycopy(encryptedMessage, 1, params, 0, paramsLength); System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength); // 创建Key final SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); SecretKey key = factory.generateSecret(keySpec); // 构建参数 AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(ALGORITHM); algorithmParameters.init(params); // 构建Cipher final Cipher cipherDecrypt = Cipher.getInstance(ALGORITHM); cipherDecrypt.init(Cipher.DECRYPT_MODE, key, algorithmParameters); return cipherDecrypt.doFinal(message); } /** * 生成指定长度的随机字节数组 * * @param length 字节数组长度 * @return 字节数组 */ private static byte[] generateSalt(int length) { byte[] salt = new byte[length]; synchronized (RANDOM) { RANDOM.nextBytes(salt); return salt; } } } ``` 接下来实现接口,对配置文件进行处理。OriginTrackedMapPropertySource这个类对应的就是application.yml的配置,我们只对它进行处理,其他的配置没有处理的必要,这一点上,jasypt是把能处理的配置都纳入了。当然人家也可以通过配置白名单跳过。 ```java /** * Spring配置文件解密,启用加密功能时,解密加密的配置项 * * @author xueye */ @Slf4j public class EncryptedPropertiesBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered { public static final String PREFIX_PROPERTY = "encryptor.prefix"; public static final String SUFFIX_PROPERTY = "encryptor.suffix"; public static final String PASSWORD_PROPERTY = "encryptor.password"; private final ConfigurableEnvironment environment; private final String prefix; private final String suffix; private final String password; public EncryptedPropertiesBeanFactoryPostProcessor(ConfigurableEnvironment environment) { this.environment = environment; // 优先从系统环境变量java -jar运行时指定的参数获取 if (StringUtils.hasText(System.getProperty(PASSWORD_PROPERTY, ""))) { password = System.getProperty(PASSWORD_PROPERTY, ""); } else if (StringUtils.hasText(environment.getProperty(PASSWORD_PROPERTY))) { password = environment.getProperty(PASSWORD_PROPERTY); } else { throw new RuntimeException("没有找到加密解密所需密码!"); } if (StringUtils.hasText(environment.getProperty(PREFIX_PROPERTY))) { prefix = environment.getProperty(PREFIX_PROPERTY); } else { prefix = "encrypt["; } if (StringUtils.hasText(environment.getProperty(SUFFIX_PROPERTY))) { suffix = environment.getProperty(SUFFIX_PROPERTY); } else { suffix = "]"; } } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { MutablePropertySources propertySources = environment.getPropertySources(); StreamSupport.stream(propertySources.spliterator(), false) .map(this::propertySourceConverter) .collect(Collectors.toList()) .forEach(props -> propertySources.replace(props.getName(), props)); } /** * 提高优先级 */ @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 100; } /** * 判断是否是加密信息 */ private boolean isEncrypted(String property) { if (property == null) { return false; } final String trimmedValue = property.trim(); return trimmedValue.startsWith(prefix) && trimmedValue.endsWith(suffix); } /** * 去除前后缀包裹 */ private String unwrapEncryptedValue(String property, String prefix, String suffix) { return property.substring(prefix.length(), property.length() - suffix.length()); } /** * 配置文件转换 */ private PropertySource> propertySourceConverter(PropertySource> propertySource) { if (!(propertySource instanceof OriginTrackedMapPropertySource)) { return propertySource; } Map decryptedProperties = new HashMap<>(); Map source = ((OriginTrackedMapPropertySource) propertySource).getSource(); for (String key : source.keySet()) { String property = environment.getProperty(key); if (isEncrypted(property)) { log.info("解密配置项:{}", key); try { String relay = unwrapEncryptedValue(property, prefix, suffix); String decrypt = EncryptUtil.decrypt(relay, password); decryptedProperties.put(key, decrypt); } catch (Exception e) { log.error("解密配置文件异常: {}", e.getMessage()); e.printStackTrace(); } } else { decryptedProperties.put(key, property); } } return new OriginTrackedMapPropertySource(propertySource.getName(), decryptedProperties, true); } } ``` 最后一步,注入Bean,让这个处理器生效即可,我很贴心的配置了一个开关,只有配置文件中对应项为true才启用加密功能。 ```java /** * 配置文件解密处理器,默认不启用,需要在配置文件中开启 * * @author xueye * @see EncryptedPropertiesBeanFactoryPostProcessor */ @Configuration @ConditionalOnProperty(name = "encryptor.enable", havingValue = "true") public class EncryptedPropertiesConfiguration { @Bean public static EncryptedPropertiesBeanFactoryPostProcessor enableEncryptedPropertySourcesPostProcessor(ConfigurableEnvironment environment) { return new EncryptedPropertiesBeanFactoryPostProcessor(environment); } } ``` 哦,忘记说配置了。如下: ```yaml mail: host: mail.qq.com protocal: smpt port: 587 sender: xueye@qq.com username: xueye password: encrypt[DzANBAgYZGAnL6IKUQIBCrT5F3H9hEtkljC1WYaWsEk=] encryptor: enable: true password: xueye.io prefix: "encrypt[" suffix: "]" ``` 如此我们的需求就完成了,比不上jasypt那般强大,但是我们需要的功能全部实现了,而且就只有一个工具类,一个处理类,和一个配置类。顺便学习了加密算法,spring原理,岂不美哉? Loading... SpringBoot作为现在最为热门的后端框架,用的人非常多,不知道你们有没有注意到,绝大部分的人application.yml配置文件里的东西都是明文密码,这其实是有风险的,比如之前就有过某某公司不小心把代码开源了,然后配置文件的里数据库密码被别有用心的人利用,导致用户隐私泄露。 我最近也遇到同样的诉求,所以研究了一下怎么才能规避这样的风险。如果你Google“如何加密SpringBoot配置文件”类似的描述,绝大部分都会推荐你使用一个叫做jasypt的插件。说实话,这是一个很强大,而且简单好用的Java加解密工具库,但是我最终没有使用。 为啥没有使用?因为没有必要,自己撸更香! ## 原理剖析 当你使用了jasypt插件,会有一些配置需要你填写,其中一个必填项就是jasypt.encryptor.password,这是用来加解密的密码。对了,jasypt默认使用的是对称加密,如果你觉得安全性不够好,也有堆对称加密可以选。我觉得默认的就足够了。我们的目的就是防君子不防小人。 另外还有的配置是加密字段的前缀和后缀,这个默认为`ENC(`和`)`,中间的就是加密后的密文。 jasypt实现了BeanFactoryPostProcessor接口,重写了postProcessBeanFactory方法,在应用启动的时候,扫描所有配置项,对配置项中符合上述前缀后缀的值进行解密,并且替换。 ## 实战 现在你已经知道了原理,非常的简单,实现起来也没有什么难度。 首先我们需要写一个工具类,用于加密和解密信息。在类变量部分,我使用固定的算法、迭代次数和盐值长度,主要是方便使用的时候少传些参数,其实完全可以作为可选参数,来实现更复杂的加密。 ### EncryptUtil ```java /** * 信息加密工具类,使用PBEWithMD5AndDES加密算法,可对有一定安全性要求的信息进行加密。 * 安全性要求较高的信息请勿使用此工具进行加密。 * * @author xueye */ public final class EncryptUtil { private EncryptUtil() { throw new AssertionError("No com.cicdi.utils.EncryptUtil instances for you!"); } /** * 随机字符生成 */ private static final SecureRandom RANDOM = new SecureRandom(); /** * 使用的加密算法 */ private static final String ALGORITHM = "PBEWithMD5AndDES"; /** * 迭代次数 */ private static final int ITERATIONS = 1000; /** * 盐值长度 */ private static final int SALT_LENGTH = 8; /** * 使用PBEWithMD5AndDES算法对信息进行加密 * * @param message 需要加密的信息 * @return 加密后的信息 */ public static String encrypt(String message, String password) { try { return Base64.getEncoder().encodeToString(encrypt(message.getBytes(StandardCharsets.UTF_8), password)); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 对使用PBEWithMD5AndDES算法加密后的信息进行解密 * * @param encryptedMessage 经过PBEWithMD5AndDES算法加密后的信息 * @return 解密后的信息 */ public static String decrypt(String encryptedMessage, String password) { try { return new String(decrypt(Base64.getDecoder().decode(encryptedMessage), password), StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null; } private static byte[] encrypt(byte[] message, String password) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IOException, BadPaddingException, IllegalBlockSizeException { // 创建Key final SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); byte[] salt = generateSalt(SALT_LENGTH); final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS); SecretKey key = factory.generateSecret(keySpec); // 构建Cipher. final Cipher cipherEncrypt = Cipher.getInstance(ALGORITHM); cipherEncrypt.init(Cipher.ENCRYPT_MODE, key); // 保存参数 byte[] params = cipherEncrypt.getParameters().getEncoded(); // 加密信息 byte[] encryptedMessage = cipherEncrypt.doFinal(message); return ByteBuffer .allocate(1 + params.length + encryptedMessage.length) .put((byte) params.length) .put(params) .put(encryptedMessage) .array(); } private static byte[] decrypt(byte[] encryptedMessage, String password) throws BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException, IOException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException { int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]); int messageLength = encryptedMessage.length - paramsLength - 1; byte[] params = new byte[paramsLength]; byte[] message = new byte[messageLength]; System.arraycopy(encryptedMessage, 1, params, 0, paramsLength); System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength); // 创建Key final SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); SecretKey key = factory.generateSecret(keySpec); // 构建参数 AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(ALGORITHM); algorithmParameters.init(params); // 构建Cipher final Cipher cipherDecrypt = Cipher.getInstance(ALGORITHM); cipherDecrypt.init(Cipher.DECRYPT_MODE, key, algorithmParameters); return cipherDecrypt.doFinal(message); } /** * 生成指定长度的随机字节数组 * * @param length 字节数组长度 * @return 字节数组 */ private static byte[] generateSalt(int length) { byte[] salt = new byte[length]; synchronized (RANDOM) { RANDOM.nextBytes(salt); return salt; } } } ``` 接下来实现接口,对配置文件进行处理。OriginTrackedMapPropertySource这个类对应的就是application.yml的配置,我们只对它进行处理,其他的配置没有处理的必要,这一点上,jasypt是把能处理的配置都纳入了。当然人家也可以通过配置白名单跳过。 ```java /** * Spring配置文件解密,启用加密功能时,解密加密的配置项 * * @author xueye */ @Slf4j public class EncryptedPropertiesBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered { public static final String PREFIX_PROPERTY = "encryptor.prefix"; public static final String SUFFIX_PROPERTY = "encryptor.suffix"; public static final String PASSWORD_PROPERTY = "encryptor.password"; private final ConfigurableEnvironment environment; private final String prefix; private final String suffix; private final String password; public EncryptedPropertiesBeanFactoryPostProcessor(ConfigurableEnvironment environment) { this.environment = environment; // 优先从系统环境变量java -jar运行时指定的参数获取 if (StringUtils.hasText(System.getProperty(PASSWORD_PROPERTY, ""))) { password = System.getProperty(PASSWORD_PROPERTY, ""); } else if (StringUtils.hasText(environment.getProperty(PASSWORD_PROPERTY))) { password = environment.getProperty(PASSWORD_PROPERTY); } else { throw new RuntimeException("没有找到加密解密所需密码!"); } if (StringUtils.hasText(environment.getProperty(PREFIX_PROPERTY))) { prefix = environment.getProperty(PREFIX_PROPERTY); } else { prefix = "encrypt["; } if (StringUtils.hasText(environment.getProperty(SUFFIX_PROPERTY))) { suffix = environment.getProperty(SUFFIX_PROPERTY); } else { suffix = "]"; } } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { MutablePropertySources propertySources = environment.getPropertySources(); StreamSupport.stream(propertySources.spliterator(), false) .map(this::propertySourceConverter) .collect(Collectors.toList()) .forEach(props -> propertySources.replace(props.getName(), props)); } /** * 提高优先级 */ @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 100; } /** * 判断是否是加密信息 */ private boolean isEncrypted(String property) { if (property == null) { return false; } final String trimmedValue = property.trim(); return trimmedValue.startsWith(prefix) && trimmedValue.endsWith(suffix); } /** * 去除前后缀包裹 */ private String unwrapEncryptedValue(String property, String prefix, String suffix) { return property.substring(prefix.length(), property.length() - suffix.length()); } /** * 配置文件转换 */ private PropertySource<?> propertySourceConverter(PropertySource<?> propertySource) { if (!(propertySource instanceof OriginTrackedMapPropertySource)) { return propertySource; } Map<String, Object> decryptedProperties = new HashMap<>(); Map<String, Object> source = ((OriginTrackedMapPropertySource) propertySource).getSource(); for (String key : source.keySet()) { String property = environment.getProperty(key); if (isEncrypted(property)) { log.info("解密配置项:{}", key); try { String relay = unwrapEncryptedValue(property, prefix, suffix); String decrypt = EncryptUtil.decrypt(relay, password); decryptedProperties.put(key, decrypt); } catch (Exception e) { log.error("解密配置文件异常: {}", e.getMessage()); e.printStackTrace(); } } else { decryptedProperties.put(key, property); } } return new OriginTrackedMapPropertySource(propertySource.getName(), decryptedProperties, true); } } ``` 最后一步,注入Bean,让这个处理器生效即可,我很贴心的配置了一个开关,只有配置文件中对应项为true才启用加密功能。 ```java /** * 配置文件解密处理器,默认不启用,需要在配置文件中开启 * * @author xueye * @see EncryptedPropertiesBeanFactoryPostProcessor */ @Configuration @ConditionalOnProperty(name = "encryptor.enable", havingValue = "true") public class EncryptedPropertiesConfiguration { @Bean public static EncryptedPropertiesBeanFactoryPostProcessor enableEncryptedPropertySourcesPostProcessor(ConfigurableEnvironment environment) { return new EncryptedPropertiesBeanFactoryPostProcessor(environment); } } ``` 哦,忘记说配置了。如下: ```yaml mail: host: mail.qq.com protocal: smpt port: 587 sender: xueye@qq.com username: xueye password: encrypt[DzANBAgYZGAnL6IKUQIBCrT5F3H9hEtkljC1WYaWsEk=] encryptor: enable: true password: xueye.io prefix: "encrypt[" suffix: "]" ``` 如此我们的需求就完成了,比不上jasypt那般强大,但是我们需要的功能全部实现了,而且就只有一个工具类,一个处理类,和一个配置类。顺便学习了加密算法,spring原理,岂不美哉? 最后修改:2023 年 08 月 02 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏