SpringBoot作为现在最为热门的后端框架,用的人非常多,不知道你们有没有注意到,绝大部分的人application.yml配置文件里的东西都是明文密码,这其实是有风险的,比如之前就有过某某公司不小心把代码开源了,然后配置文件的里数据库密码被别有用心的人利用,导致用户隐私泄露。

我最近也遇到同样的诉求,所以研究了一下怎么才能规避这样的风险。如果你Google“如何加密SpringBoot配置文件”类似的描述,绝大部分都会推荐你使用一个叫做jasypt的插件。说实话,这是一个很强大,而且简单好用的Java加解密工具库,但是我最终没有使用。

为啥没有使用?因为没有必要,自己撸更香!

原理剖析

当你使用了jasypt插件,会有一些配置需要你填写,其中一个必填项就是jasypt.encryptor.password,这是用来加解密的密码。对了,jasypt默认使用的是对称加密,如果你觉得安全性不够好,也有堆对称加密可以选。我觉得默认的就足够了。我们的目的就是防君子不防小人。

另外还有的配置是加密字段的前缀和后缀,这个默认为ENC(),中间的就是加密后的密文。

jasypt实现了BeanFactoryPostProcessor接口,重写了postProcessBeanFactory方法,在应用启动的时候,扫描所有配置项,对配置项中符合上述前缀后缀的值进行解密,并且替换。

实战

现在你已经知道了原理,非常的简单,实现起来也没有什么难度。

首先我们需要写一个工具类,用于加密和解密信息。在类变量部分,我使用固定的算法、迭代次数和盐值长度,主要是方便使用的时候少传些参数,其实完全可以作为可选参数,来实现更复杂的加密。

EncryptUtil

/**
 * 信息加密工具类,使用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是把能处理的配置都纳入了。当然人家也可以通过配置白名单跳过。

/**
 * 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才启用加密功能。

/**
 * 配置文件解密处理器,默认不启用,需要在配置文件中开启
 *
 * @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);
    }
}

哦,忘记说配置了。如下:

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 日
如果觉得我的文章对你有用,请随意赞赏