Spring Boot配置中心敏感数据加密解决方案

Spring Boot配置中心敏感数据加密解决方案

在微服务架构中,配置中心往往存储着数据库密码、API密钥等敏感信息。
本文介绍两种Spring Boot环境下的配置加密方案。
1)使用jasypt-spring-boot-starter方案
2)基于EnvironmentPostProcessor自定义加解密方案


方案一:使用jasypt-spring-boot-starter

实现步骤

1.添加依赖
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
2.添加properties/yml配置项
jasypt.encryptor.bootstrap=true
jasypt.encryptor.password=d9d41b8469A-ca4b3db8caS15f28065e55d
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.salt-generator-classname=org.jasypt.salt.RandomSaltGenerator
jasypt.encryptor.iv-generator-classname=org.jasypt.iv.RandomIvGenerator

注:password在生产环境需要单独配置,可以单独指定生产的classpath,或在运行时通过-D参数动态指定passowrd

3.使用
import javax.annotation.PostConstruct;

import org.jasypt.encryption.StringEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @Value("${spring.datasource.password}")
    private String password;

    @Autowired
    private StringEncryptor encryptor;

    @GetMapping("/encrypt")
    public Object encrypt(@RequestParam("data") String data) {
        return encryptor.encrypt(data);
    }


    @PostConstruct
    public void init() {
        log.info("password:{}", password);
    }

}


调用encrypt接口,获取密文,将密文配置在配置文件中,例如ENC(密文),应用在启动时会自动解密。

优势

  • 开箱即用:无需编写加解密逻辑
  • 标准化:支持多种加密算法(PBE、AES等)
  • 生态完善:与Spring Cloud Config无缝集成
  • 安全可靠:使用随机盐值加密,相同明文每次加密结果不同

方案二:自定义BeanFactoryPostProcessor

Spring Boot在启动时会加载各种PropertySource,包括本地配置文件、环境变量、命令行参数等,大部分配置中心都是通过实现自己的EnvironmentPostProcessor或ApplicationContextInitializer,并在其中添加自己的PropertySource,所以如果需要自己来实现应用启动自动解密,必须要在配置中心将配置文件加载之后,我们才能对加载进来的property进行二次迭代。

代码


import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Base64Utils;

/**
 * 读取环境变量,将需要解密的属性进行解密。
 * 使用方法如下
 * 1. 使用当前文件的Security类的encrypt方法对敏感数据进行加密后,将加密后的密文配置到配置文件中,格式为PV(密文)
 * 2. 加密的密钥,可以在运行时设置java环境变量enc.key.data=x, 若不设置默认使用default_key_data
 */
@Configuration
public class ConfigEnvironmentPostProcessor implements BeanFactoryPostProcessor {

    private static final String PREFIX = "PV(";
    private static final String SUFFIX = ")";
    private static final String DECRYPTED_PROPERTY_SOURCE_PREFIX = "decrypted-";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        ConfigurableEnvironment environment = beanFactory.getBean(ConfigurableEnvironment.class);
        decryptEnvironment(environment);
    }

    private void decryptEnvironment(ConfigurableEnvironment environment) {

        List<PropertySource<?>> sources = new ArrayList<>();
        environment.getPropertySources().forEach(sources::add);

        for (PropertySource<?> source : sources) {
            Map<String, Object> decryptedProps = new HashMap<>();
            collectDecryptedProperties(source, decryptedProps);

            if (!decryptedProps.isEmpty()) {
                environment.getPropertySources().addFirst(
                    new MapPropertySource(DECRYPTED_PROPERTY_SOURCE_PREFIX + source.getName(), decryptedProps)
                );
            }
        }
    }

    private void collectDecryptedProperties(PropertySource<?> source,
        Map<String, Object> decryptedProps) {
        if (source instanceof CompositePropertySource) {
            ((CompositePropertySource) source).getPropertySources()
                .forEach(s -> collectDecryptedProperties(s, decryptedProps));
        } else if (source instanceof EnumerablePropertySource) {
            EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
            Arrays.stream(enumerable.getPropertyNames())
                .filter(key -> source.getProperty(key) instanceof String)
                .forEach(key -> {
                    String value = (String) source.getProperty(key);
                    if (value.startsWith(PREFIX) && value.endsWith(SUFFIX)) {
                        String strValue = value.substring(PREFIX.length(), value.length() - SUFFIX.length());
                        // 可以换成自己的加解密工具
                        decryptedProps.put(key, Security.decrypt(strValue));
                    }
                });
        }
    }

    public static class Security {
        private static final String static_version = "P";
        private static final String key_data = "enc.key.data";
        private static final String default_key_data = "qazwsx";
        public static String decrypt(String data) {
            try {
                data = secDecrypt(data, getKeyData());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return data;
        }

        private static String encrypt(String data) {
            try {
                data = secEncrypt(data, getKeyData());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return data;
        }

        private static byte[] getKeyData() {
            String property = System.getProperty(key_data);
            if (StringUtils.isBlank(property)) {
                return default_key_data.getBytes();
            }
            return property.getBytes();
        }

        private static String secDecrypt(String encryptStr, byte[] pkey) throws Exception {
            if (StringUtils.isBlank(encryptStr)) {
                return null;
            }
            int index = encryptStr.indexOf("-");
            String tempStr = encryptStr;
            if (index > 0) {
                tempStr = encryptStr.substring(index + 1);
            }
            return aesDecryptByBytes(Base64Utils.decode(tempStr.getBytes()), pkey);
        }

        private static String aesDecryptByBytes(byte[] encryptBytes, byte[] encryptKey) throws Exception {
            KeyGenerator kgen = KeyGenerator.getInstance("AES");
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(encryptKey);
            kgen.init(128, random);
            SecretKey secretKey = new SecretKeySpec(kgen.generateKey().getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            byte[] decryptBytes = cipher.doFinal(encryptBytes);
            return new String(decryptBytes);
        }

        private static String secEncrypt(String content, byte[] pkey) throws Exception {
            if (StringUtils.isBlank(content)) {
                return null;
            }
            return static_version + "-" + Base64Utils.encodeToString(aesEncryptToBytes(content, pkey));
        }

        private static byte[] aesEncryptToBytes(String content, byte[] encryptKey) throws Exception {
            KeyGenerator kgen = KeyGenerator.getInstance("AES");
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(encryptKey);
            kgen.init(128, random);
            SecretKey secretKey = new SecretKeySpec(kgen.generateKey().getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            return cipher.doFinal(content.getBytes("UTF-8"));
        }

        public static void main(String[] args) {
            //String decryptResult = Security.decrypt("密文");
            //System.out.println("解密结果:" + decryptResult);

            // 输入明文
            String encryptResult = Security.encrypt("明文");
            // 将输出的结果放入配置文件
            System.out.println("加密结果:" + encryptResult);
        }
    }

}

该类实现了BeanFactoryPostProcessor,会在BeanDefinition加载之后,Bean实例化之前执行,在这时所有的配置文件均已加载完毕。可以根据environment.getPropertySources()获取到所有的配置文件,在逐一进行遍历,依据前缀和后缀的标识决定是否进行解密操作。

优势

  • 完全可控:可自定义加密算法(示例使用AES)
  • 灵活标记:支持PV()前缀等自定义标识
  • 深度集成:可在属性加载阶段进行干预

劣势

  • 实现复杂:需处理PropertySource的遍历逻辑
  • 维护成本:需要自行保证加密实现的安全性
  • 兼容性问题:对CompositePropertySource需要特殊处理

方案对比

维度jasypt方案自定义方案
开发效率⭐⭐⭐⭐⭐(快速集成)⭐⭐⭐⭐⭐(快速集成)
安全性⭐⭐⭐⭐(社区验证)⭐⭐(依赖自身实现)
可维护性⭐⭐⭐⭐(标准方案)⭐(需持续维护)
算法扩展性⭐⭐(配置修改)⭐⭐⭐⭐(代码级修改)
与配置中心耦合度⭐(无特殊处理)⭐⭐⭐(可深度定制)

选型建议

选择jasypt方案:

  • 需要快速落地加密方案
  • 项目组没有密码学专家
  • 使用标准加密算法即可满足需求

选择自定义方案:

  • 需要与公司安全规范对齐
  • 有特殊加密算法要求
  • 需要深度定制属性加载逻辑
  • 配置中心有特殊的加密格式要求

提醒

使用时,无论是哪种方案,在生产环境上,都不建议写死加解密的密钥,可以参考如下配置。
1.运行时通过-D参数动态指定
2.直接指定classpath,将配置文件单独在服务器存储
3.修改源代码,将读取密钥的逻辑改为从指定目录的文件中获取
4.使用HSM(Hardware Security Module)硬件加密模块处理密钥

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值