给配置文件中的密码加密,使用jasypt-配置分析源码分析

803 阅读6分钟

0、参考文章

1、配置分析

使用jasypt-spring-boot-starter来进行配置文件密码加密

走个流程,分析一波他的自动配置过程

<!--用于给配置文件中的密码等加密-->
<!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

看到这个依赖很简洁哇,没有额外的依赖

image.png

分析自动配置原理

流程就是从自动配置类xxxAutoConfiguration-->自动配置类里面注册或者导入了组件 -->xxxProperties里面有属性值-->绑定了配置文件中以xxx.xxxx开头的配置项

//自动配置类
@Configuration
@Import(EnableEncryptablePropertiesConfiguration.class)
public class JasyptSpringBootAutoConfiguration {
}

//导入的组件的 EnableEncryptablePropertiesConfiguration
//这里还是没有出现配置绑定,继续找,从导入的这两个类中看,先看EncryptablePropertyResolverConfiguration
@Configuration
@Import({EncryptablePropertyResolverConfiguration.class, CachingConfiguration.class})
@Slf4j
public class EnableEncryptablePropertiesConfiguration {

    //里面只注册了这一个组件
    @Bean
    public static EnableEncryptablePropertiesBeanFactoryPostProcessor enableEncryptablePropertySourcesPostProcessor(final ConfigurableEnvironment environment, EncryptablePropertySourceConverter converter) {
        return new EnableEncryptablePropertiesBeanFactoryPostProcessor(environment, converter);
    }
}

EncryptablePropertyResolverConfiguration,这个类里面注册了很多组件,其中有一个组件为:

@Configuration
public class EncryptablePropertyResolverConfiguration {

    //其中有一个bean为,看一下这个单例的泛型JasyptEncryptorConfigurationProperties
    //这个名字比较明显了,就是配置类,所以点进去
    @Bean(name = CONFIG_SINGLETON)
    public Singleton<JasyptEncryptorConfigurationProperties> configProps(
            final EnvCopy envCopy) {
        return new Singleton<>(() -> JasyptEncryptorConfigurationProperties.bindConfigProps(envCopy.get()));
    }

}

JasyptEncryptorConfigurationProperties:激动人心吧,终于找到了配置绑定类。可以看到绑定了配置文件jasypt.encryptor开头的属性,所以直接在application.yaml文件中该属性就可以了

@ConfigurationProperties(prefix = "jasypt.encryptor", ignoreUnknownFields = true)
@Data
public class JasyptEncryptorConfigurationProperties {
    
    //这里简单列一下几个属性
    
       /**
     * Specify the name of bean to override jasypt-spring-boot's default properties based
     * {@link org.jasypt.encryption.StringEncryptor}. Default Value is {@code jasyptStringEncryptor}.
     */
    //指定加密器对应的bean名称
    private String bean = "jasyptStringEncryptor";
    
    //里面默认的算法为:
    private String algorithm = "PBEWITHHMACSHA512ANDAES_256";
    
    //密匙没有默认值,需要我们在配置文件中指定
    private String password;
    
    //字符串输出的编码格式
	private String stringOutputType = "base64";
    
    //嵌套的配置属性,在jasypt.encryptor基础上再加上.property
    @NestedConfigurationProperty
    private PropertyConfigurationProperties property = new PropertyConfigurationProperties();

    @Data
    public static class PropertyConfigurationProperties {

        //前缀
        private String prefix = "ENC(";
        
        //后缀
        private String suffix = ")";
        
    }
}

那么我们看一下生成密匙的工具类

EncryptablePropertyResolverConfiguration中进行的注册,所以我们使用的时候直接@Autowired

返回的加密器是DefaultLazyEncryptor类型,简单看一下他的构造器参数:

  • envCopy.get()获取了配置文件中的参数
  • customEncryptorBeanName用户自定义加密器的bean名称
  • isCustom用户是否自定义了加密器,jasypt.encryptor.bean默认有值为jasyptStringEncryptor,也可以在配置文件中指定
  • bfBean工厂
@Configuration
public class EncryptablePropertyResolverConfiguration {
    
    private static final String ENCRYPTOR_BEAN_PROPERTY = "jasypt.encryptor.bean";
    
    private static final String ENCRYPTOR_BEAN_PLACEHOLDER = String.format("${%s:jasyptStringEncryptor}", ENCRYPTOR_BEAN_PROPERTY);
    
    private static final String ENCRYPTOR_BEAN_NAME = "lazyJasyptStringEncryptor";

    @Bean(name = ENCRYPTOR_BEAN_NAME)
    public StringEncryptor stringEncryptor(
            final EnvCopy envCopy,
            final BeanFactory bf) {
        final String customEncryptorBeanName = envCopy.get().resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER);
        final boolean isCustom = envCopy.get().containsProperty(ENCRYPTOR_BEAN_PROPERTY);
        return new DefaultLazyEncryptor(envCopy.get(), customEncryptorBeanName, isCustom, bf);
    }
}

2、开始使用

使用的大致流程是,我们根据加密规则生成明文的密码,然后用这些密码代替原来的明文。那么系统启动的时候会根据我们的配置进行解密。

2.1 配置文件

jasypt:
  encryptor:
#    加密密匙
    password: zylai

在这个配置文件中指定密匙

2.2 加密明文

我们直接在测试类中获取配置文件中的明文,然后加密

@SpringBootTest
public class EncryptTest {

    //加密的工具
    @Autowired
    private StringEncryptor stringEncryptor;

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void getEncryptedPwd(){
        Environment environment = applicationContext.getBean(Environment.class);
        //获取配置文件中的明文密码
        String mysqlPassword = environment.getProperty("spring.datasource.password");
        String redisPassword = environment.getProperty("spring.redis.password");
        System.out.println("明文信息为如下:");
        System.out.println("mysqlPassword:"+mysqlPassword+"\nredisPassword:"+redisPassword);

        System.out.println("========加密之后的信息如下===============");
        System.out.println("mysqlPassword:"+encrypt(mysqlPassword));
        System.out.println("redisPassword:"+encrypt(redisPassword));
    }

    public String encrypt(String str){
        return stringEncryptor.encrypt(str);
    }
}

image.png

2.3 修改配置文件

将上面的加密字符串代替配置文件中的明文即可,注意需要将加密的结果放到ENC()里面,为什么是ENC呢?这个是默认的设置,程序启动时会去解析这个括号里面的密码。

spring.redis.password=ENC(T5os7CbvjZ3N33Ns96ikjfSIFByMRhxhBNb+G6qWLhbn3vpZ0miP+HKfRQDiyhts)
spring.datasource.password=ENC(6eAFfiW1eGeYH+enb1A0rmcsI+Jw5hFZe5Earb2Fu3SrwaDQaXbZ8DASDTbjI5R7)

其实我们可以自定义ENC(),只需要添加属性即可

jasypt:
  encryptor:
#    加密密匙
    password: zylai
    property:
      prefix: ZYLai(
      suffix: )

在第一部分自动配置分析中,从配置绑定类的属性就可以看出我们可以自定义

接下来直接启动程序验证一下即可,然后在主启动类中,我们再去获取一下这些密码看看程序是否能够自动解密(只要程序启动成功一般就是成功了)

@SpringBootApplication
public class Boot04WebAdminApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Boot04WebAdminApplication.class, args);
        Environment environment = run.getBean(Environment.class);
        //查看一下加密的密匙,测试用
        System.out.println(environment.getProperty("jasypt.encryptor.password"));

        //在程序启动时,jasypt会解密ENC()中的内容
        String mysqlPassword = environment.getProperty("spring.datasource.password");
        String mysqlUsername = environment.getProperty("spring.datasource.username");
        System.out.println("测试数据加密,解密之后的mysql账号密码为:");
        System.out.println("mysqlPassword:"+mysqlPassword+"\nmysqlUsername:"+mysqlUsername);
    }

}

大功告成!

image.png

2.4 启动程序的日志

同时在启动的日志中可以看到,如果没有进行配置值,就会使用默认的

image.png

这个日志是在加密器DefaultLazyEncryptor中执行的:

@Slf4j
public class DefaultLazyEncryptor implements StringEncryptor {

    private final Singleton<StringEncryptor> singleton;

    public DefaultLazyEncryptor(final ConfigurableEnvironment e, final String customEncryptorBeanName, boolean isCustom, final BeanFactory bf) {
        singleton = new Singleton<>(() ->
                Optional.of(customEncryptorBeanName)
                        .filter(bf::containsBean)
                        .map(name -> (StringEncryptor) bf.getBean(name))
                        .map(tap(bean -> log.info("Found Custom Encryptor Bean {} with name: {}", bean, customEncryptorBeanName)))
                        .orElseGet(() -> {
                            if (isCustom) {
                                throw new IllegalStateException(String.format("String Encryptor custom Bean not found with name '%s'", customEncryptorBeanName));
                            }
                            log.info("String Encryptor custom Bean not found with name '{}'. Initializing Default String Encryptor", customEncryptorBeanName);
                            return createDefault(e);
                        }));
    }

    public DefaultLazyEncryptor(final ConfigurableEnvironment e) {
        singleton = new Singleton<>(() -> createDefault(e));
    }

    private StringEncryptor createDefault(ConfigurableEnvironment e) {
        return new StringEncryptorBuilder(JasyptEncryptorConfigurationProperties.bindConfigProps(e), "jasypt.encryptor").build();
    }

    @Override
    public String encrypt(final String message) {
        return singleton.get().encrypt(message);
    }

    @Override
    public String decrypt(final String encryptedMessage) {
        return singleton.get().decrypt(encryptedMessage);
    }

}

那么如何自定义这个属性呢?在第四部分详细展开

3、配置文件加密了?但又没完全加密

虽然在配置文件中没有密码等敏感信息了,但是他又加密的密匙jasypt.encryptor.password哇,别人仍然可以拿着这个密匙进行解密来获取你的敏感信息。所以密匙可以不写在配置文件中!!!

三种方式

方式一:直接作为程序启动时的命令行参数来带入

java -jar yourproject.jar --jasypt.encryptor.password=zylai
  • 方式二:直接作为程序启动时的应用环境变量来带入
java -Djasypt.encryptor.password=zyali -jar yourproject.jar
复制代码

对于已经完成的项目进行加密可以使用以上两种方式,但对于个人正在开发的项目,可以试试使用系统的环境变量。

  • 方式三:甚至可以作为系统环境变量的方式来带入

我们提前设置好系统环境变量JASYPT_ENCRYPTOR_PASSWORD = zylai,则直接在Spring Boot的项目配置文件中做如下配置即可:

jasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD}

4、自定义功能-分析源码

加密器分析

在配置类JasyptEncryptorConfigurationProperties中可以看到指定了加密器对应的bean名称

   /**
     * Specify the name of bean to override jasypt-spring-boot's default properties based
     * {@link org.jasypt.encryption.StringEncryptor}. Default Value is {@code jasyptStringEncryptor}.
     */
    //指定加密器对应的bean名称
    private String bean = "jasyptStringEncryptor";

官方的注释说指定bean名称来重写默认的加密器,所以我们自己配置的加密器的名称最好也是这个名字,或者在配置文件中显式指定这个名字

那么这个bean是在哪里注册的呢?我们再回到EncryptablePropertyResolverConfiguration,这个类里面注册了很多组件

值得注意的是,这个StringEncryptor接口bean的名称为lazyJasyptStringEncryptor,并不是jasyptStringEncryptor,但是他的返回值(即实现类)是一个DefaultLazyEncryptor,其构造器中指定了这个bean的名称

分析一下这个方法的返回值内容new DefaultLazyEncryptor(envCopy.get(), customEncryptorBeanName, isCustom, bf);

  • customEncryptorBeanName指定了用户定义加密器的bean名称,名称为String.format("${%s:jasyptStringEncryptor}", ENCRYPTOR_BEAN_PROPERTY),就是从ENCRYPTOR_BEAN_PROPERTY中获取,如果没有值就使用jasyptStringEncryptor
  • isCustom:用户是否自定义了,判断方式为:envCopy.get().containsProperty(ENCRYPTOR_BEAN_PROPERTY);,在配置类中有默认值,我们也可以在配置文件中指定
@Configuration
public class EncryptablePropertyResolverConfiguration {
    
    private static final String ENCRYPTOR_BEAN_PROPERTY = "jasypt.encryptor.bean";
    
    private static final String ENCRYPTOR_BEAN_PLACEHOLDER = String.format("${%s:jasyptStringEncryptor}", ENCRYPTOR_BEAN_PROPERTY);
    
    private static final String ENCRYPTOR_BEAN_NAME = "lazyJasyptStringEncryptor";

    @Bean(name = ENCRYPTOR_BEAN_NAME)
    public StringEncryptor stringEncryptor(
            final EnvCopy envCopy,
            final BeanFactory bf) {
        final String customEncryptorBeanName = envCopy.get().resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER);
        final boolean isCustom = envCopy.get().containsProperty(ENCRYPTOR_BEAN_PROPERTY);
        return new DefaultLazyEncryptor(envCopy.get(), customEncryptorBeanName, isCustom, bf);
    }
}

我们再看一下DefaultLazyEncryptor构造器的执行为:

@Slf4j
public class DefaultLazyEncryptor implements StringEncryptor {

    private final Singleton<StringEncryptor> singleton;

    public DefaultLazyEncryptor(final ConfigurableEnvironment e, final String customEncryptorBeanName, boolean isCustom, final BeanFactory bf) {
        singleton = new Singleton<>(() ->
                Optional.of(customEncryptorBeanName)
                        .filter(bf::containsBean)
                        .map(name -> (StringEncryptor) bf.getBean(name))
                        .map(tap(bean -> log.info("Found Custom Encryptor Bean {} with name: {}", bean, customEncryptorBeanName)))
                        .orElseGet(() -> {
                            if (isCustom) {
                                throw new IllegalStateException(String.format("String Encryptor custom Bean not found with name '%s'", customEncryptorBeanName));
                            }
                            log.info("String Encryptor custom Bean not found with name '{}'. Initializing Default String Encryptor", customEncryptorBeanName);
                            return createDefault(e);
                        }));
    }

    public DefaultLazyEncryptor(final ConfigurableEnvironment e) {
        singleton = new Singleton<>(() -> createDefault(e));
    }

    private StringEncryptor createDefault(ConfigurableEnvironment e) {
        return new StringEncryptorBuilder(JasyptEncryptorConfigurationProperties.bindConfigProps(e), "jasypt.encryptor").build();
    }

    @Override
    public String encrypt(final String message) {
        return singleton.get().encrypt(message);
    }

    @Override
    public String decrypt(final String encryptedMessage) {
        return singleton.get().decrypt(encryptedMessage);
    }

}

一套分析下来应该也明白了,就是我们可以自定义加密器(bean的返回值类型要是StringEncryptor),加密器bean的默认名称为jasyptStringEncryptor,如果我们自定了名称,需要在配置文件中指定jasypt.encryptor.bean

自定义的加密器

密匙一定要从配置文件中获取,要是自己再写明文,那不是白忙活了吗

@Configuration
public class EncryptorConfig {

    @Bean("jasyptStringEncryptor")
    public StringEncryptor zylaiEncryptor(@Value("${jasypt.encryptor.password}") String password){

        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(password);
        config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
//        config.setProviderName("zylai");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        return encryptor;
    }
}

5、关于解密

是否需要自定义解密器

是否有个问题,我们自定义了加密器,那么解密器是不是也要自己定义一个,来对应加密器?答案是完全不用哈

首先,加密器的接口StringEncryptor,长这个样子。看见没,不仅仅只是加密,也有解密的方法

public interface StringEncryptor {
    String encrypt(String var1);

    String decrypt(String var1);
}

再看一下如何向容器中注册的解密器:

这个方法和注册加密器的方法几乎一样,支持用户的自定义,返回DefaultLazyPropertyResolver实例,但是他的构造器多了一个参数为encryptor,这个参数就是从容器中获取的@Qualifier(ENCRYPTOR_BEAN_NAME) final StringEncryptor encryptor,哈哈经典的ENCRYPTOR_BEAN_NAME,又回来了吧,所以说加密和解密都是基于容器中名为ENCRYPTOR_BEAN_NAME,类型为StringEncryptor 的组件

@Bean(name = RESOLVER_BEAN_NAME)
public EncryptablePropertyResolver encryptablePropertyResolver(
        @Qualifier(DETECTOR_BEAN_NAME) final EncryptablePropertyDetector propertyDetector,
        @Qualifier(ENCRYPTOR_BEAN_NAME) final StringEncryptor encryptor, final BeanFactory bf,
        final EnvCopy envCopy, final ConfigurableEnvironment environment) {
    final String customResolverBeanName = envCopy.get().resolveRequiredPlaceholders(RESOLVER_BEAN_PLACEHOLDER);
    final boolean isCustom = envCopy.get().containsProperty(RESOLVER_BEAN_PROPERTY);
    return new DefaultLazyPropertyResolver(propertyDetector, encryptor, customResolverBeanName, isCustom, bf, environment);
}

解密的方法

上面看到容器中会注册一个DefaultLazyPropertyResolver组件,构造器中单例最终创建的是DefaultPropertyResolver类型

EncryptablePropertyResolver 接口 就俩实现类,除了这一个就是DefaultPropertyResolver,在DefaultPropertyResolver中真正实现了解密

@Slf4j
public class DefaultLazyPropertyResolver implements EncryptablePropertyResolver {

    private Singleton<EncryptablePropertyResolver> singleton;

    public DefaultLazyPropertyResolver(EncryptablePropertyDetector propertyDetector, StringEncryptor encryptor, String customResolverBeanName, boolean isCustom, BeanFactory bf, Environment environment) {
        singleton = new Singleton<>(() ->
                Optional.of(customResolverBeanName)
                        .filter(bf::containsBean)
                        .map(name -> (EncryptablePropertyResolver) bf.getBean(name))
                        .map(tap(bean -> log.info("Found Custom Resolver Bean {} with name: {}", bean, customResolverBeanName)))
                        .orElseGet(() -> {
                            if (isCustom) {
                                throw new IllegalStateException(String.format("Property Resolver custom Bean not found with name '%s'", customResolverBeanName));
                            }
                            log.info("Property Resolver custom Bean not found with name '{}'. Initializing Default Property Resolver", customResolverBeanName);
                            return createDefault(propertyDetector, encryptor, environment);
                        }));
    }

    public DefaultLazyPropertyResolver(EncryptablePropertyDetector propertyDetector, StringEncryptor encryptor, Environment environment) {
        singleton = new Singleton<>(() -> createDefault(propertyDetector, encryptor, environment));
    }

    //创建DefaultPropertyResolver对象
    private DefaultPropertyResolver createDefault(EncryptablePropertyDetector propertyDetector, StringEncryptor encryptor, Environment environment) {
        return new DefaultPropertyResolver(encryptor, propertyDetector, environment);
    }

    @Override
    public String resolvePropertyValue(String value) {
        return singleton.get().resolvePropertyValue(value);
    }
}

DefaultPropertyResolver类中主要看resolvePropertyValue他会根据StringEncryptor实例中的decrypt方法进行解密,并将解密之后的结果再放回配置环境中,也就有了之前我们看到程序启动之后,在启动类中直接获取配置文件中加密的属性的明文。

public class DefaultPropertyResolver implements EncryptablePropertyResolver {

    private final Environment environment;
    private StringEncryptor encryptor;
    private EncryptablePropertyDetector detector;

    public DefaultPropertyResolver(StringEncryptor encryptor, Environment environment) {
        this(encryptor, new DefaultPropertyDetector(), environment);
    }

    public DefaultPropertyResolver(StringEncryptor encryptor, EncryptablePropertyDetector detector, Environment environment) {
        this.environment = environment;
        Assert.notNull(encryptor, "String encryptor can't be null");
        Assert.notNull(detector, "Encryptable Property detector can't be null");
        this.encryptor = encryptor;
        this.detector = detector;
    }

    @Override
    public String resolvePropertyValue(String value) {
        return Optional.ofNullable(value)
                .map(environment::resolvePlaceholders)
                .filter(detector::isEncrypted)
                .map(resolvedValue -> {
                    try {
                        String unwrappedProperty = detector.unwrapEncryptedValue(resolvedValue.trim());
                        String resolvedProperty = environment.resolvePlaceholders(unwrappedProperty);
                        return encryptor.decrypt(resolvedProperty);
                    } catch (EncryptionOperationNotPossibleException e) {
                        throw new DecryptionException("Unable to decrypt: " + value + ". Decryption of Properties failed,  make sure encryption/decryption " +
                                "passwords match", e);
                    }
                })
                .orElse(value);
    }
}