百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

编程实践:使用Jasypt加密SpringBoot配置文件

bigegpt 2024-08-31 16:51 2 浏览

连接:https://mp.weixin.qq.com/s/jw_AQ6sm7O3SkeDImWB3jg

作者:sunshujie1990



1.构建一个springboot项目,并且引入jasypt依赖

<dependency>
????????<groupId>com.github.ulisesbocchio</groupId>
????????<artifactId>jasypt-spring-boot-starter</artifactId>
????????<version>3.0.2</version>
</dependency>

2.编写一个单元测试,用于获取加密后的账号密码

StringEncryptor是jasypt-spring-boot-starter自动配置的加密工具,加密算法我们选择PBEWithHmacSHA512AndAES_128,password为123abc

jasypt.encryptor.password=123abc
jasypt.encryptor.algorithm=PBEWithHmacSHA512AndAES_128


@SpringBootTest
class?SpringbootPropertiesEncApplicationTests?{

????@Autowired
????private?StringEncryptor?stringEncryptor;

????@Test
????void?contextLoads()?{
????????String?sunshujie?=?stringEncryptor.encrypt("sunshujie");
????????String?qwerty1234?=?stringEncryptor.encrypt("qwerty1234");
????????System.out.println(sunshujie);
????????System.out.println(qwerty1234);
????}

}

3.在application.properties中配置加密后的账号密码

jasypt.encryptor.password=123abc
jasypt.encryptor.algorithm=PBEWithHmacSHA512AndAES_128
username=ENC(pXDnpH3GdMDBHdxraKyAt7IKCeX8mVlM9A9PeI9Ow2VUoBHRESQ5m8qhrbp45vH+)
password=ENC(qD55H3EKYpxp9cGBqpOfR2pqD/AgqT+IyClWKdW80MkHx5jXEViaJTAx6Es4/ZJt)

4.观察在程序中是否能够拿到解密后的账号密码

@SpringBootApplication
public?class?SpringbootPropertiesEncApplication?implements?CommandLineRunner?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(SpringbootPropertiesEncApplication.class);
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(SpringbootPropertiesEncApplication.class,?args);
????}

????@Value("${password}")
????private?String?password;
????@Value("${username}")
????private?String?username;

????@Override
????public?void?run(String...?args)?throws?Exception?{
????????logger.info("username:?{}?,?password:?{}?",?username,?password);
????}
}

原理解析

加密原理

首先看jasypt相关的配置,分别是password和加密算法

jasypt.encryptor.password=123abc
jasypt.encryptor.algorithm=PBEWithHmacSHA512AndAES_128

PBEWithHmacSHA512AndAES_128是此次我们选用的加密算法.

123abc是PBEWithHmacSHA512AndAES_128加密过程中用的加密密码.

PBE是基于密码的加密算法,密码和秘钥相比有什么好处呢?好处就是好记…

PBE加密流程如下

  1. 密码加盐
  2. 密码加盐结果做摘要获取秘钥
  3. 用秘钥对称加密原文,然后和盐拼在一起得到密文

PBE解密流程如下

  1. 从密文获取盐
  2. 密码+盐摘要获取秘钥
  3. 密文通过秘钥解密获取原文

再来看PBEWithHmacSHA512AndAES_128,名字就是加密过程中用的具体算法

  • PBE是指用的是PBE加密算法
  • HmacSHA512是指摘要算法,用于获取秘钥
  • AES_128是对称加密算法

jasypt-spring-boot-starter原理

先从spring.factories文件入手查看自动配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ulisesbocchio.jasyptspringboot.JasyptSpringBootAutoConfiguration

JasyptSpringBootAutoConfiguration配置仅仅使用@Import注解引入另一个配置类EnableEncryptablePropertiesConfiguration.

@Configuration
@Import({EnableEncryptablePropertiesConfiguration.class})
public?class?JasyptSpringBootAutoConfiguration?{
????public?JasyptSpringBootAutoConfiguration()?{
????}
}

从配置类EnableEncryptablePropertiesConfiguration可以看到有两个操作

1.@Import了EncryptablePropertyResolverConfiguration.class, CachingConfiguration.class

2.注册了一个BeanFactoryPostProcessor -> EnableEncryptablePropertiesBeanFactoryPostProcessor

@Configuration
@Import({EncryptablePropertyResolverConfiguration.class,?CachingConfiguration.class})
public?class?EnableEncryptablePropertiesConfiguration?{
????private?static?final?Logger?log?=?LoggerFactory.getLogger(EnableEncryptablePropertiesConfiguration.class);

????public?EnableEncryptablePropertiesConfiguration()?{
????}

????@Bean
????public?static?EnableEncryptablePropertiesBeanFactoryPostProcessor?enableEncryptablePropertySourcesPostProcessor(ConfigurableEnvironment?environment)?{
????????boolean?proxyPropertySources?=?(Boolean)environment.getProperty("jasypt.encryptor.proxy-property-sources",?Boolean.TYPE,?false);
????????InterceptionMode?interceptionMode?=?proxyPropertySources???InterceptionMode.PROXY?:?InterceptionMode.WRAPPER;
????????return?new?EnableEncryptablePropertiesBeanFactoryPostProcessor(environment,?interceptionMode);
????}
}

先看EncryptablePropertyResolverConfiguration.class

lazyEncryptablePropertyDetector这里有配置文件中ENC()写法的出处.从名称来看是用来找到哪些配置需要解密.

从代码来看,不一定非得用ENC()把密文包起来, 也可以通过配置来指定其他前缀和后缀

jasypt.encryptor.property.prefix
jasypt.encryptor.property.suffix
????@Bean(
????????name?=?{"lazyEncryptablePropertyDetector"}
????)
????public?EncryptablePropertyDetector?encryptablePropertyDetector(EncryptablePropertyResolverConfiguration.EnvCopy?envCopy,?BeanFactory?bf)?{
????????String?prefix?=?envCopy.get().resolveRequiredPlaceholders("${jasypt.encryptor.property.prefix:ENC(}");
????????String?suffix?=?envCopy.get().resolveRequiredPlaceholders("${jasypt.encryptor.property.suffix:)}");
????????String?customDetectorBeanName?=?envCopy.get().resolveRequiredPlaceholders(DETECTOR_BEAN_PLACEHOLDER);
????????boolean?isCustom?=?envCopy.get().containsProperty("jasypt.encryptor.property.detector-bean");
????????return?new?DefaultLazyPropertyDetector(prefix,?suffix,?customDetectorBeanName,?isCustom,?bf);
????}

另外还配置了很多bean,先记住这两个重要的bean.带着疑问往后看.

  • lazyEncryptablePropertyResolver 加密属性解析器
  • lazyEncryptablePropertyFilter 加密属性过滤器
????@Bean(
????????name?=?{"lazyEncryptablePropertyResolver"}
????)
????public?EncryptablePropertyResolver?encryptablePropertyResolver(@Qualifier("lazyEncryptablePropertyDetector")?EncryptablePropertyDetector?propertyDetector,?@Qualifier("lazyJasyptStringEncryptor")?StringEncryptor?encryptor,?BeanFactory?bf,?EncryptablePropertyResolverConfiguration.EnvCopy?envCopy,?ConfigurableEnvironment?environment)?{
????????String?customResolverBeanName?=?envCopy.get().resolveRequiredPlaceholders(RESOLVER_BEAN_PLACEHOLDER);
????????boolean?isCustom?=?envCopy.get().containsProperty("jasypt.encryptor.property.resolver-bean");
????????return?new?DefaultLazyPropertyResolver(propertyDetector,?encryptor,?customResolverBeanName,?isCustom,?bf,?environment);
????}

????@Bean(
????????name?=?{"lazyEncryptablePropertyFilter"}
????)
????public?EncryptablePropertyFilter?encryptablePropertyFilter(EncryptablePropertyResolverConfiguration.EnvCopy?envCopy,?ConfigurableBeanFactory?bf,?@Qualifier("configPropsSingleton")?Singleton<JasyptEncryptorConfigurationProperties>?configProps)?{
????????String?customFilterBeanName?=?envCopy.get().resolveRequiredPlaceholders(FILTER_BEAN_PLACEHOLDER);
????????boolean?isCustom?=?envCopy.get().containsProperty("jasypt.encryptor.property.filter-bean");
????????FilterConfigurationProperties?filterConfig?=?((JasyptEncryptorConfigurationProperties)configProps.get()).getProperty().getFilter();
????????return?new?DefaultLazyPropertyFilter(filterConfig.getIncludeSources(),?filterConfig.getExcludeSources(),?filterConfig.getIncludeNames(),?filterConfig.getExcludeNames(),?customFilterBeanName,?isCustom,?bf);
????}

再看EnableEncryptablePropertiesBeanFactoryPostProcessor这个类

  1. 是一个BeanFactoryPostProcessor
  2. 实现了Ordered,是最低优先级,会在其他BeanFactoryPostProcessor执行之后再执行
  3. postProcessBeanFactory方法中获取了上面提到的两个重要的bean, lazyEncryptablePropertyResolver lazyEncryptablePropertyFilter
  4. 从environment中获取了PropertySources
  5. 调用工具类进行转换PropertySources, 也就是把密文转换为原文
public?class?EnableEncryptablePropertiesBeanFactoryPostProcessor?implements?BeanFactoryPostProcessor,?Ordered?{????
????//?ignore?some?code
????public?void?postProcessBeanFactory(ConfigurableListableBeanFactory?beanFactory)?throws?BeansException?{
????????LOG.info("Post-processing?PropertySource?instances");
????????EncryptablePropertyResolver?propertyResolver?=?(EncryptablePropertyResolver)beanFactory.getBean("lazyEncryptablePropertyResolver",?EncryptablePropertyResolver.class);
????????EncryptablePropertyFilter?propertyFilter?=?(EncryptablePropertyFilter)beanFactory.getBean("lazyEncryptablePropertyFilter",?EncryptablePropertyFilter.class);
????????MutablePropertySources?propSources?=?this.environment.getPropertySources();
????????EncryptablePropertySourceConverter.convertPropertySources(this.interceptionMode,?propertyResolver,?propertyFilter,?propSources);
????}

????public?int?getOrder()?{
????????return?2147483547;
????}
}

再看工具类EncryptablePropertySourceConverter

1.过滤所有已经是EncryptablePropertySource的PropertySource

2.转换为EncryptablePropertySource

3.用EncryptablePropertySource从PropertySources中替换原PropertySource

????public?static?void?convertPropertySources(InterceptionMode?interceptionMode,?EncryptablePropertyResolver?propertyResolver,?EncryptablePropertyFilter?propertyFilter,?MutablePropertySources?propSources)?{
????????((List)StreamSupport.stream(propSources.spliterator(),?false).filter((ps)?->?{
????????????return?!(ps?instanceof?EncryptablePropertySource);
????????}).map((ps)?->?{
????????????return?makeEncryptable(interceptionMode,?propertyResolver,?propertyFilter,?ps);
????????}).collect(Collectors.toList())).forEach((ps)?->?{
????????????propSources.replace(ps.getName(),?ps);
????????});
????}

关键方法在makeEncryptable中,调用链路很长, 这里选取一条链路跟一下

  1. .ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter#makeEncryptable
  2. com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter#convertPropertySource
  3. com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter#proxyPropertySource
  4. com.ulisesbocchio.jasyptspringboot.aop.EncryptablePropertySourceMethodInterceptor#invoke
  5. com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource#getProperty
  6. com.ulisesbocchio.jasyptspringboot.EncryptablePropertySource#getProperty()

看到最后豁然开朗,发现就是用的最开始配置的DefaultLazyPropertyResolver进行密文解析.

直接看最终的实现 DefaultPropertyResolver

  1. 据lazyEncryptablePropertyDetector过滤需要解密的配置
  2. 用lazyEncryptablePropertyDetector去掉前缀后缀
  3. 替换占位符
  4. 解密
????public?String?resolvePropertyValue(String?value)?{
????????Optional?var10000?=?Optional.ofNullable(value);
????????Environment?var10001?=?this.environment;
????????var10001.getClass();
????????var10000?=?var10000.map(var10001::resolveRequiredPlaceholders);
????????EncryptablePropertyDetector?var2?=?this.detector;
????????var2.getClass();
????????return?(String)var10000.filter(var2::isEncrypted).map((resolvedValue)?->?{
????????????try?{
????????????????String?unwrappedProperty?=?this.detector.unwrapEncryptedValue(resolvedValue.trim());
????????????????String?resolvedProperty?=?this.environment.resolveRequiredPlaceholders(unwrappedProperty);
????????????????return?this.encryptor.decrypt(resolvedProperty);
????????????}?catch?(EncryptionOperationNotPossibleException?var5)?{
????????????????throw?new?DecryptionException("Unable?to?decrypt:?"?+?value?+?".?Decryption?of?Properties?failed,??make?sure?encryption/decryption?passwords?match",?var5);
????????????}
????????}).orElse(value);
????}

解惑

1.加密配置文件能否使用摘要算法,例如md5?

不能, 配置文件加密是需要解密的,例如数据库连接信息加密,如果不解密,springboot程序无法读取到真正的数据库连接信息,也就无法建立连接.

2.加密配置文件能否直接使用对称加密,不用PBE?

可以, PBE的好处就是密码好记.

3.jasypt.encryptor.password可以泄漏吗?

不能, 泄漏了等于没有加密.

4.例子中jasypt.encryptor.password配置在配置文件中不就等于泄漏了吗?

是这样的,需要在流程上进行控制.springboot打包时千万不要把jasypt.encryptor.password打入jar包内.

在公司具体的流程可能是这样的:

  • 运维人员持有jasypt.encryptor.password,加密原文获得密文
  • 运维人员将密文发给开发人员
  • 开发人员在配置文件中只配置密文,不配置jasypt.encryptor.password
  • 运维人员启动应用时再配置jasypt.encryptor.password

如果有其他疑惑欢迎留言提问, 另外由于作者水平有限难免有疏漏, 欢迎留言纠错。

相关推荐

Java 泛型大揭秘:类型参数、通配符与最佳实践

引言在编程世界中,代码的可重用性和可维护性是至关重要的。为了实现这些目标,Java5引入了一种名为泛型(Generics)的强大功能。本文将详细介绍Java泛型的概念、优势和局限性,以及如何在...

K8s 的标签与选择器:流畅运维的秘诀

在Kubernetes的世界里,**标签(Label)和选择器(Selector)**并不是最炫酷的技术,但却是贯穿整个集群管理与运维流程的核心机制。正是它们让复杂的资源调度、查询、自动化运维变得...

哈希Hash算法:原理、应用(哈希算法 知乎)

原作者:Linux教程,原文地址:「链接」什么是哈希算法?哈希算法(HashAlgorithm),又称为散列算法或杂凑算法,是一种将任意长度的数据输入转换为固定长度输出值的数学函数。其输出结果通常被...

C#学习:基于LLM的简历评估程序(c# 简历)

前言在pocketflow的例子中看到了一个基于LLM的简历评估程序的例子,感觉还挺好玩的,为了练习一下C#,我最近使用C#重写了一个。准备不同的简历:image-20250528183949844查...

55顺位,砍41+14+3!季后赛也成得分王,难道他也是一名球星?

雷霆队最不可思议的新星:一个55号秀的疯狂逆袭!你是不是也觉得NBA最底层的55号秀,就只能当饮水机管理员?今年的55号秀阿龙·威金斯恐怕要打破你的认知了!常规赛阶段,这位二轮秀就像开了窍的天才,直接...

5分钟读懂C#字典对象(c# 字典获取值)

什么是字典对象在C#中,使用Dictionary类来管理由键值对组成的集合,这类集合被称为字典。字典最大的特点就是能够根据键来快速查找集合中的值,其键的定义不能重复,具有唯一性,相当于数组索引值,字典...

c#窗体传值(c# 跨窗体传递数据)

在WinForm编程中我们经常需要进行俩个窗体间的传值。下面我给出了两种方法,来实现传值一、在输入数据的界面中定义一个属性,供接受数据的窗体使用1、子窗体usingSystem;usingSyst...

C#入门篇章—委托(c#委托的理解)

C#委托1.委托的定义和使用委托的作用:如果要把方法作为函数来进行传递的话,就要用到委托。委托是一个类型,这个类型可以赋值一个方法的引用。C#的委托通过delegate关键字来声明。声明委托的...

C#.NET in、out、ref详解(c#.net framework)

简介在C#中,in、ref和out是用于修改方法参数传递方式的关键字,它们决定了参数是按值传递还是按引用传递,以及参数是否必须在传递前初始化。基本语义对比修饰符传递方式可读写性必须初始化调用...

C#广义表(广义表headtail)

在C#中,广义表(GeneralizedList)是一种特殊的数据结构,它是线性表的推广。广义表可以包含单个元素(称为原子),也可以包含另一个广义表(称为子表)。以下是一个简单的C#广义表示例代...

「C#.NET 拾遗补漏」04:你必须知道的反射

阅读本文大概需要3分钟。通常,反射用于动态获取对象的类型、属性和方法等信息。今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的。获取类型的成员Type类的GetMembe...

C#启动外部程序的问题(c#怎么启动)

IT&OT的深度融合是智能制造的基石。本公众号将聚焦于PLC编程与上位机开发。除理论知识外,也会结合我们团队在开发过程中遇到的具体问题介绍一些项目经验。在使用C#开发上位机时,有时会需要启动外部的一些...

全网最狠C#面试拷问:这20道题没答出来,别说你懂.NET!

在竞争激烈的C#开发岗位求职过程中,面试是必经的一道关卡。而一场高质量的面试,不仅能筛选出真正掌握C#和.NET技术精髓的人才,也能让求职者对自身技术水平有更清晰的认知。今天,就为大家精心准备了20道...

C#匿名方法(c#匿名方法与匿名类)

C#中的匿名方法是一种没有名称只有主体的方法,它提供了一种传递代码块作为委托参数的技术。以下是关于C#匿名方法的一些重要特点和用法:特点省略参数列表:使用匿名方法可省略参数列表,这意味着匿名方法...

C# Windows窗体(.Net Framework)知识总结

Windows窗体可大致分为Form窗体和MDI窗体,Form窗体没什么好细说的,知识点总结都在思维导图里面了,下文将围绕MDI窗体来讲述。MDI(MultipleDocumentInterfac...