SpringBoot 中利用PostProcessor进行配置加密

SpringBoot 中加密某些配置

Spring Boot does not provide any built in support for encrypting property values, however, it does provide the hook points necessary to modify values contained in the Spring Environment. The EnvironmentPostProcessor interface allows you to manipulate the Environment before the application starts. 

上面这段文字是SpringBoot官方文档中,对属性加密介绍。粗略翻译为:SpringBoot没有为加密属性值提供任何内置支持,但是它提供了修改Spring Environment中值所需的钩子。EnvironmentPostProcessor允许您在应用程序启动之前操作Environment

我的理解:EnvironmentPostProcessor执行时机是在应用程序启动时,环境加载完成后与容器初始化前,依据是SpringApplication类中run(…)方法,下面是run(…)方法的一段源码:

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
					
			// 这里开始加载应用程序启动所需信息存储在ConfigurableEnvironment
			// 包含使用properties/yml的配置
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			
			// 然而这里才初始化容器
			// 默认创建 AnnotationConfigApplicationContext
			// web环境创建 AnnotationConfigServletWebServerApplicationContext/AnnotationConfigReactiveWebServerApplicationContext
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			// 刷新容器,实际上是调用父类的 refresh()方法,这里才开始创建Spring管理的Bean
            // 包括自动配置创建的Bean也是在此处创建的
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

由上面的源码,实现EnvironmentPostProcessor中postProcessEnvironment()方法,在配置信息加载完成后,使用之前,可以对加密配置项进行修改,这样的好处是不需要重写原有的自动装配的类。

创建EnvironmentPostProcessor接口的实现,注意在resources下创建META-INF目录,并在META-INF下创建spring.factories文件,配置org.springframework.boot.env.EnvironmentPostProcessor=全类名,这步非常重要,否则不生效,例如这里org.example.extend.CustomEnvironmentPostProcessor

package org.example.extend;

import org.example.util.EncryptUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor {

    private static Logger LOGGER = LoggerFactory.getLogger(CustomEnvironmentPostProcessor.class);

    /** 加密项的前缀,标号此值为加密后的值,需要解密 */
    private static String PREFIX = "AES:";

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        String secretKey = environment.getProperty("custom.secret.key");
        if (secretKey == null || secretKey.length() == 0) {
            return;
        }

        // properties/yml配置文件加载后都存储在OriginTrackedMapPropertySource中。
        List<OriginTrackedMapPropertySource> originTrackedMapPropertySourceList = environment.getPropertySources()
                .stream().filter(propertySource -> propertySource instanceof OriginTrackedMapPropertySource)
                .map(propertySource -> (OriginTrackedMapPropertySource) propertySource).collect(Collectors.toList());

        Map<String, Object> propertiesMap = new HashMap<>(8);
        for (OriginTrackedMapPropertySource source : originTrackedMapPropertySourceList) {
            // 依次遍历OriginTrackedMapPropertySource,获取其中所有的配置key,
            // 通过key获取值,判断值是否有AES:的前缀,有的则为加密,需要解码。
            for (String propertyName : source.getPropertyNames()) {
                Object propertyValue = source.getProperty(propertyName);
                if (propertyValue instanceof String) {
                    String value = (String) propertyValue;
                    if (value.startsWith(PREFIX)) {
                        propertiesMap.put(propertyName, decrypt(value, secretKey));
                    }
                }
            }
        }
        
        // 到此处已将所有的加密项全部解码,只需要将其解码后的放入environment里PropertySource中。
        // 注意一定放在靠前位置,最好第一位,原因是调用getProperty()方法获取值时,是依次遍历PropertySources,找到key匹配的且值不为空的。
        environment.getPropertySources().addFirst(new MapPropertySource("custom-decrypt", propertiesMap));
    }

    private String decrypt(String encode, String secretKey) {
        String plaintext = null;
        try {
            encode = encode.substring(PREFIX.length());
            plaintext = EncryptUtils.decryptByAES(encode, secretKey);
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error("解密发生错误:{}", e.getMessage(), e);
        }

        return plaintext;
    }

}

加密使用的是AES对称加密,附上代码:

package org.example.util;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class EncryptUtils {

    public static String encryptByAES(String content, String secretKey) throws Exception {
        byte[] enSecretKey = getSecretKeyByAES(secretKey);
        SecretKey key = new SecretKeySpec(enSecretKey, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] byteEncode = content.getBytes("utf-8");
        byte[] byteAES = cipher.doFinal(byteEncode);
        String encode = new BASE64Encoder().encode(byteAES);
        return encode;
    }

    public static String decryptByAES(String encrypt, String secretKey) throws Exception {
        byte[] enSecretKey = getSecretKeyByAES(secretKey);
        SecretKey key = new SecretKeySpec(enSecretKey, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] byteContent = new BASE64Decoder().decodeBuffer(encrypt);
        byte[] byteDecode = cipher.doFinal(byteContent);
        String AESDecode = new String(byteDecode, "utf-8");
        return AESDecode;
    }

    private static byte[] getSecretKeyByAES(String secretKey) throws NoSuchAlgorithmException {
        KeyGenerator generator = KeyGenerator.getInstance("AES");
        generator.init(256, new SecureRandom(secretKey.getBytes()));
        SecretKey originalKey = generator.generateKey();
        return originalKey.getEncoded();
    }

}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值