spring boot使用jasypt加密原理解析

版本对应的坑

关键技术点

源码解析

将jar包引入到spring boot中

@EnableAutoConfiguration原理

JasyptSpringBootAutoConfiguration

一是其@Import的StringEncryptorConfiguration.class

二是其对spring环境中包含的PropertySource对象的处理

一是AbstractApplicationContext的refresh方法

二是BeanFactoryPostProcessor接口的作用

EnableEncryptablePropertySourcesPostProcessor

具体的解密过程

补充1:查看JDK提供的Cipher算法

补充2:PBE的基础算法demo,

参考:

首先介绍一下jasypt的使用方法

可以参考下面这篇文章:

Get史上最优雅的加密方式!没有之一!

版本对应的坑
使用的时候还是遇到一个坑,就是jasypt的版本与spring boot版本存在对应情况。可以看到jasypt是区分java7和java8的,也存在依赖spring版本的情况。

自己尝试了一下

在使用jasypt-spring-boot-starter的前提下

jasypt版本 springboot版本 2.1.0 2.1.0 1.5 1.4.2 1.5 1.5.3 1.8 1.4.2

所以如果引入maven之后启动系统报错,那么可以根据版本对应情况这个角度进行排查。

关键技术点
下面说一下jasypt的两个关键的技术实现点

一是如何实现对spring环境中包含的PropertySource对象实现加密感知的

二是其默认的PBEWITHMD5ANDDES算法是如何工作的,并澄清一下在使用jasypt的时候最常遇到的一个疑问:既然你的password也配置在properties文件中,那么我拿到了加密的密文和password,不是可以直接解密吗?

源码解析
总结来说:其通过BeanFactoryPostProcessor#postProcessBeanFactory方法,获取所有的propertySource对象,将所有propertySource都会重新包装成新的EncryptablePropertySourceWrapper

解密的时候,也是使用EncryptablePropertySourceWrapper#getProperty方法,如果通过 prefixes/suffixes 包裹的属性,那么返回解密后的值;如果没有被包裹,那么返回原生的值。从源头开始走起:

将jar包引入到spring boot中

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ulisesbocchio.jasyptspringboot.JasyptSpringBootAutoConfiguration
这里补充一下spring boot @EnableAutoConfiguration的原理。

@EnableAutoConfiguration原理
@EnableAutoConfiguration注解@Import(AutoConfigurationImportSelector.class)

这个配置类实现了ImportSelector接口,重写其selectImports方法

List

protected List

  1. ImportSelector 该接口的方法的返回值都会被纳入到spring容器的管理中

  2. SpringFactoriesLoader 该类可以从classpath中搜索所有META-INF/spring.factories配置文件,读取配置

@EnableAutoConfiguration注解中有spring.boot.enableautoconfiguration=true就开启,默认为true,可以在application.properties中设置此开关项

exclude()方法是根据类排除,excludeName是根据类名排除

在spring-boot-autoconfigure jar中,META-INF中有一个spring.factories文件,其中配置了spring-boot所有的自动配置参数,如GsonAutoConfiguration,配合@ConditionalOnClass(Gson.class),可以实现如果Gson bean存在,就启动自动注入,否则就不启用此注入的灵活配置

好了,有了上面的基础知识,我们就关心JasyptSpringBootAutoConfiguration

JasyptSpringBootAutoConfiguration
其@Import EnableEncryptablePropertySourcesConfiguration

关注两个地方

一是其@Import的StringEncryptorConfiguration.class
如果没有自定义的EncryptorBean,即jasyptStringEncryptor bean,那么就注册默认的jasyptStringEncryptor bean

@Conditional(OnMissingEncryptorBean.class)
@Bean(name = ENCRYPTOR_BEAN_PLACEHOLDER)
public StringEncryptor stringEncryptor(Environment environment) {
String encryptorBeanName = environment.resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER);
LOG.info(“String Encryptor custom Bean not found with name ‘{}’. Initializing String Encryptor based on properties with name ‘{}'”,
encryptorBeanName, encryptorBeanName);
return new LazyStringEncryptor(() -> {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(getRequiredProperty(environment, “jasypt.encryptor.password”));
config.setAlgorithm(getProperty(environment, “jasypt.encryptor.algorithm”, “PBEWithMD5AndDES”));
config.setKeyObtentionIterations(getProperty(environment, “jasypt.encryptor.keyObtentionIterations”, “1000”));
config.setPoolSize(getProperty(environment, “jasypt.encryptor.poolSize”, “1”));
config.setProviderName(getProperty(environment, “jasypt.encryptor.providerName”, “SunJCE”));
config.setSaltGeneratorClassName(getProperty(environment, “jasypt.encryptor.saltGeneratorClassname”, “org.jasypt.salt.RandomSaltGenerator”));
config.setStringOutputType(getProperty(environment, “jasypt.encryptor.stringOutputType”, “base64”));
encryptor.setConfig(config);
return encryptor;
});
}
StringEncryptor接口提供了加密和解密的方法

我们可以自定义StringEncryptor,如

@Configuration
public class JasyptConfig {

@Bean(name = “jasypt.encryptor.bean:jasyptStringEncryptor”)
public StringEncryptor stringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(“password”);
config.setAlgorithm(“PBEWithMD5AndDES”);
config.setKeyObtentionIterations(“1000”);
config.setPoolSize(“1”);
config.setProviderName(“SunJCE”);
config.setSaltGeneratorClassName(“org.jasypt.salt.RandomSaltGenerator”);
config.setStringOutputType(“base64”);
encryptor.setConfig(config);
return encryptor;
}
}
二是其对spring环境中包含的PropertySource对象的处理
@Configuration
@Import(StringEncryptorConfiguration.class)
public class EnableEncryptablePropertySourcesConfiguration implements EnvironmentAware {

private static final Logger LOG = LoggerFactory.getLogger(EnableEncryptablePropertySourcesConfiguration.class);
private ConfigurableEnvironment environment;

@Bean
public EnableEncryptablePropertySourcesPostProcessor enableEncryptablePropertySourcesPostProcessor() {
boolean proxyPropertySources = environment.getProperty(“jasypt.encryptor.proxyPropertySources”, Boolean.TYPE, false);
InterceptionMode interceptionMode = proxyPropertySources ? InterceptionMode.PROXY : InterceptionMode.WRAPPER;
return new EnableEncryptablePropertySourcesPostProcessor(environment, interceptionMode);
}

@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
}
其提供了两种模式来创建 分别为proxy和wrapper 默认情况下interceptionMode为wrapper

下面就是关键了,new了一个EnableEncryptablePropertySourcesPostProcessor

其implements BeanFactoryPostProcessor

这里又需要两个背景知识

一是AbstractApplicationContext的refresh方法
是启动spring容器的关键方法

// Allows post-processing of the bean factory in context subclasses.

postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.

invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.

registerBeanPostProcessors(beanFactory);
来注册我们下面的postProcessors

二是BeanFactoryPostProcessor接口的作用
BeanFactoryPostProcessor接口提供了postProcessBeanFactory方法,在容器初始化之后执行一次

invokeBeanFactoryPostProcessors,获取的手动注册的BeanFactoryPostProcessor

/**
* Invoke the given BeanFactoryPostProcessor beans.

*/
private static void invokeBeanFactoryPostProcessors(
Collection postProcessors, ConfigurableListableBeanFactory beanFactory) {

for (BeanFactoryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanFactory(beanFactory);
}
}
可以看到postProcessors有4个

接下来看关键的EnableEncryptablePropertySourcesPostProcessor

EnableEncryptablePropertySourcesPostProcessor
public class EnableEncryptablePropertySourcesPostProcessor implements BeanFactoryPostProcessor, ApplicationListener

其中getOrder方法 让这个jasypt定义的BeanFactoryPostProcessor的初始化顺序最低,即最后初始化

我们知道spring中排序分为两种PriorityOrdered 和Ordered接口,一般来说就是PriorityOrdered 优于Ordered 其次都是按照order大小来的排序

我们就知道了接下来就执行EnableEncryptablePropertySourcesPostProcessor的postProcessBeanFactory方法,

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
LOG.info(“Post-processing PropertySource instances”);
MutablePropertySources propSources = environment.getPropertySources();
StreamSupport.stream(propSources.spliterator(), false)
.filter(ps -> !(ps instanceof EncryptablePropertySource))
.map(s -> makeEncryptable(s, beanFactory))
.collect(toList())
.forEach(ps -> propSources.replace(ps.getName(), ps));
}
接下来,获取所有的propertySource对象

然后用stream方式遍历,如果是通过jasypt加密的,那么来执行方法makeEncryptable,使得propertySource对象具备加密解密的能力

private

private

log日志:将上面的6个对象包装一下

最后的application.properties中的配置项结果

完整的转换完成后的EncryptablePropertySourceWrapper

到这里就注册postProcessor完成了,而且每个PropertySource warpped,具备了加密解密的能力,然后继续回到AbstractApplicationContext的流程

// Instantiate all remaining (non-lazy-init) singletons.

finishBeanFactoryInitialization(beanFactory);
具体的解密过程
当spring boot项目启动的时候,需要用到属性值的时候,就是将原本spring中的propertySource的getProperty()方法委托给其自定义的实现EncryptablePropertySourceWrapper,调用其getProperty()方法,在这个方法的自定义实现中。判断是否是已经加密的value,如果是,则进行解密。如果不是,那就返回原值。

调用EncryptablePropertySourceWrapper的getProperty方法,其extends PropertySource,override了getProperty方法

public class EncryptablePropertySourceWrapper

public interface EncryptablePropertySource

private static final String ENCRYPTED_VALUE_PREFIX = “ENC(“;
private static final String ENCRYPTED_VALUE_SUFFIX = “)”;

public static boolean isEncryptedValue(final String value) {
if (value == null) {
return false;
}
final String trimmedValue = value.trim();
return (trimmedValue.startsWith(ENCRYPTED_VALUE_PREFIX) &&
trimmedValue.endsWith(ENCRYPTED_VALUE_SUFFIX));
}
如果通过 prefixes/suffixes 包裹的属性,那么返回解密后的值;

如果没有被包裹,那么返回原生的值;

如果是加密的值,那么就去解密

StandardPBEByteEncryptor

public byte[] decrypt(final byte[] encryptedMessage)
throws EncryptionOperationNotPossibleException {

if (encryptedMessage == null) {
return null;
}

// Check initialization
if (!isInitialized()) {
initialize();
}

if (this.saltGenerator.includePlainSaltInEncryptionResults()) {
// Check that the received message is bigger than the salt
if (encryptedMessage.length

明文是root

密文是ENC(X4OZ4csEAWqPCEvWf+aRPA==)

可以看到其salt是encryptedMessage的

System.arraycopy(encryptedMessage, saltStart, salt, 0, saltSize);
System.arraycopy(encryptedMessage, encMesKernelStart, encryptedMessageKernel, 0, encMesKernelSize);
0-7byte解析为salt,8-15byte解析为密文

然后就通过基本的PBE解析方式,来解析出来

ASCII码对应的结果就是root

PBE解析原理图:

加密过程:每一次随机产生新的salt,所以每一次加密后生成的密文是不同的

解密过程:

所以我们就可以知道,如果我获得了jasypt的password,那么由于其salt是放在encryptedMessage中的,那么我是没什么压力就可以解密的。

所以应该java -jar –Djasypt.encryptor.password=xxx abc.jar方式来启动服务。这样只要在运维端不泄露password,那么只拿到配置文件的密文,还是安全的。

补充1:查看JDK提供的Cipher算法
jasypt默认使用的是PBEWITHMD5ANDDES,其实JDK中由SunJCE所提供的。

可以通过下面的代码来查看JDK中提供了哪些Cipher算法

@Test
public void listJdkAlgorithm() {
/ Provider[] providers = Security.getProviders();
for (Provider provider :
providers) {
LOGGER.info(“security provider: {} , version: {}”, provider.getName(), provider.getVersion());
LOGGER.info(“security provider info: {}”, provider.getInfo());
}
/
Set

Standard Algorithm Name Documentation

补充2:PBE的基础算法demo,
而且可以看出来,jasypt中使用了几乎相同的代码来进行加解密的

public class PBECipher {

static final String CIPHER_NAME = “PBEwithMD5AndDES”;

public static byte[] encrypt(String password, byte[] salt, byte[] input) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(CIPHER_NAME);
// 这个secretKey 就是我们将来要使用的加密的密钥
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
// 传入1000,表示用户输入的口令,会与这个salt进行1000次的循环
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
return cipher.doFinal(input);
}

public static byte[] decrypt(String password, byte[] salt, byte[] input) throws NoSuchAlgorithmException,
InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(CIPHER_NAME);
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
return cipher.doFinal(input);
}
}
测试

@Test
public void testPBE() throws NoSuchAlgorithmException, UnsupportedEncodingException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException {
String message = “constfafa”;
String password = “ydbs”;
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(8);
System.out.printf(“salt: %032x\n”, new BigInteger(1, salt));

//加密和解密的salt是一样的
byte[] data = message.getBytes(“UTF-8”);
byte[] encrypt = PBECipher.encrypt(password, salt, data);
LOGGER.info(“encrypted data: {}”, Base64.getEncoder().encodeToString(encrypt));

byte[] decrypt = PBECipher.decrypt(password, salt, encrypt);
LOGGER.info(“decrypted data: {}”, new String(decrypt,”UTF-8″));
}
参考:
Jasypt之源码解析

官方github

8.Java 加解密技术系列之 PBE – crazyYong – 博客园
————————————————
版权声明:本文为CSDN博主「const伐伐」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013905744/article/details/86508236

@Import注解的应用和扩展

Original: https://www.cnblogs.com/softidea/p/14533532.html
Author: 沧海一滴
Title: spring boot使用jasypt加密原理解析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/540553/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Mysql傻瓜式安装教程,Mysql GUI SQLyog安装教程,java数据库连接JDBC(Eclipse)

    一、Mysql的安装 1.截至2022/5/22最新版mysql安装器。 https://cdn.mysql.com//Downloads/MySQLInstaller/mysql…

    Java 2023年6月5日
    0117
  • 分布式锁的三种实现方式

    上图中,我们部署了两个Tomcat,共同支撑系统。当一个请求到达系统时,首先会经过Nginx,Nginx主要是做负载转发的,它会根据自己配置的负载均衡策略将请求转发到其中的一个To…

    Java 2023年6月13日
    097
  • JVM详解

    一、JVM的位置及体系结构 JVM作用在操作系统之上,而Java程序作用在jvm之上,其他的程序则与jvm并列 二、类加载器,及双亲委派机制 1.类加载器 作用:加载Class文件…

    Java 2023年6月13日
    087
  • MySQL学习之路(1):SQL脚本语言

    使用MySQL数据库,首先安装MySQL数据库,本文所有SQL脚本在MySQL上测试和执行。 安装Mysql服务器;安装Mysql workbench客户端,可以以图形化界面管理m…

    Java 2023年6月6日
    076
  • XenServer中备份正在运行的虚拟机

    本篇文章介绍的内容是关于如何在XenServer中备份正在运行的虚拟机 ,并且可以逐步运行VM的备份过程,此外还有一个shell脚本,可以将所有VM备份或指定的VM备份,我们也可以…

    Java 2023年5月30日
    0109
  • 如何使用Java代码修改数组大小呢?

    数组是Java开发中非常重要的一个数据存储容器, 那可以存储多种类型,基础类型,引用类型,但是它有一个缺点,就是一旦创建后,就不可以修改数组的大小, 那么我们如何动态的扩容数组的大…

    Java 2023年6月15日
    0101
  • Java 常用类总结(SE基础)

    本篇博客对java常用类相关知识进行了归纳总结,比较详细,适用于学习和复习。 字符串相关的类 1.1 String String是一个 final类,代表不可变的字符序列。不可被继…

    Java 2023年6月7日
    0111
  • 第一次

    看到很多编程高手都是博客院的账号,我也注册了一个帐号,希望多交流一些技术问题。请大家多多关照。 Original: https://www.cnblogs.com/thinkgem…

    Java 2023年6月7日
    099
  • dubbo源码分析5(dubbo服务暴露入口)

    经过了前面这么多的铺垫,没错,前面说了这么一大堆的都是铺垫,我们说了spi,以及基于spring的扩展,这一篇就开始说说dubbo吧! 1.dubbo的配置文件解析 在第一篇的时候…

    Java 2023年6月6日
    0117
  • 11.多线程、多进程和线程池编程

    1.1.线程同步Lock和Rlock (1)Lock 用锁会影响性能 用锁会产生死锁 import threading from threading import Lock tot…

    Java 2023年5月29日
    0217
  • 【PHP】熟悉php对应的DES相关加解密,与java、C#对接加解密工程

    1、记一次与java和C# 对接DES加解密工程; 2、主要是用到DES的相关加密方案,链接:https://docs.mallcoo.cn/PosPoints/APIDoc_OP…

    Java 2023年5月29日
    092
  • java 双因素认证(2FA)TOTP demo

    TOTP 的全称是”基于时间的一次性密码”(Time-based One-time Password)。它是公认的可靠解决方案,已经写入国际标准 RFC62…

    Java 2023年6月16日
    0107
  • Spring Boot 实现 RabbitMQ 延迟消费和延迟重试队列

    本文主要摘录自:详细介绍Spring Boot + RabbitMQ实现延迟队列 并增加了自己的一些理解,记录下来,以便日后查阅。 项目源码: spring-boot-rabbit…

    Java 2023年5月30日
    0153
  • 这个开源组织里的项目都是精品(第二弹)

    前言 之前我写过一篇文章——《这个开源组织里的项目都是精品》,里面列举了Dromara开源组织的4个java项目,每一个都轻量且实用,受到了很多小伙伴的喜爱。Dromara这个开源…

    Java 2023年6月8日
    0109
  • 20220812-Java内部类

    跟老韩学完了java面向对象的高级篇,老师提到了卖油翁和老黄牛的故事,在学习Java的路上,借以自勉,”我亦无他,唯手熟尔”,天道酬勤,长路漫漫,少年加油,…

    Java 2023年6月15日
    097
  • Java面试题(三)–虚拟机

    1 内存结构 1、简述一下JVM的内存结构?(高频) JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大…

    Java 2023年6月9日
    0100
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球