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();
    }

}

你可能感兴趣的:(Java)