|

历史文章(文章累计440+)《国内最全的Spring Boot系列之一》《国内最全的Spring Boot系列之二》《国内最全的Spring Boot系列之三》《国内最全的Spring Boot系列之四》《国内最全的Spring Boot系列之五》配置类信息赋值为Java静态变量「扩展点实战系列》」- 第441篇
3种方案扩展RestTemplate让其具备负载均衡(超级详细)「扩展点实战系列」- 第442篇一个注解@LoadBalanced就能让RestTemplate拥有负载均衡的能力?「扩展点实战系列」- 第443篇Spring注解@Qualifier的详细用法你知道几种「扩展点实战系列」- 第444篇利用Spring扩展点模拟Feign实现远程调用(干货满满)「扩展点实战系列」- 第445篇深入Feign源码吃透Spring扩展点「扩展点实战系列」- 第446篇
悟纤:师傅,你在前面讲到了配置文件加密的方式有2种?
师傅:说2种也可以,说1种也可以,本质的话都是Spring的扩展点来实现的。

师傅:或许,我们换一种方式来表达会更好,配置文件敏感信息的保护是自己使用Spring扩展点实现,还是使用开源的第三方框架来实现。

悟纤:那我们不使用第三方框架,自己实现呢?应该怎么做呢?
师傅:徒儿,你这小马达开启了,就霹雳哗啦的问一堆问题了,但这个习惯不错,值得表扬,时刻保持好奇心是成长的关键。

悟纤:难得能够得到师傅的表扬,让我高兴万分。
师傅:今天为师这一文,会集中n多的以前的知识点,你好好听讲,将会受益匪浅。
悟纤:骑上我心爱的小摩托,带上我的小板凳… 学啊学,学啊学,学到天荒地老,学到海枯石烂。

导读
在前面的一篇文章《SpringBoot配置文件内容加密,实现敏感信息保护》谈到,配置文件内容敏感信息保护有2种常见的方案,我们讲到了其中的一种,这一节来使用另外一种方案,也就是使用Spring的扩展点进行敏感信息的保护。
扩展点实战系列:
01.《观察者模式实际应用场景「扩展点实战系列」》
02.《服务信息上报+记录请求信息+监听项目运行状态还能这么玩 「扩展点实战系列」》
03.《配置类信息赋值为Java静态变量「扩展点实战系列》」》
04. 《3种方案扩展RestTemplate让其具备负载均衡(超级详细)「扩展点实战系列」》
05.《一个注解@LoadBalanced就能让RestTemplate拥有负载均衡的能力?「扩展点实战系列」》
06.《Spring注解@Qualifier的详细用法你知道几种「扩展点实战系列」》
07.《利用Spring扩展点模拟FeignClient实现远程调用(干货满满)「扩展点实战系列」》
08.《深入FeignClient源码吃透Spring扩展点「扩展点实战系列」》
09.《SpringBoot配置文件内容加密,实现敏感信息保护「扩展点实战系列」》
10.《利用Spring扩展点对敏感信息加密解密,一文集齐n多知识点「扩展点实战系列」》
11.「待拟定」《利用Spring扩展点模拟MyBatis的注解编程「扩展点实战系列」》
一、思路以及编码分析
在开始具体的编码之前,咱们先来简单的分析一下思路以及编码要考虑的一些点。
1.1思路分析
我们来分析一下,对于配置文件内容要加密,要解决的几个问题
(1)其一就是内容加密以及解密,所以需要有一个加密算法,加密算法又分对称加密和非对称加密。
(2)其二就是如何区分配置文件里的内容哪些加密了,哪些未加密。对于这一点的话,可以使用特殊的标识符标记,比如使用[ 加密数据 ]对加密信息进行包裹
(3)其三就是在哪一个扩展点进行解密。这里主要使用到的扩展点是BeanFactoryPostProcessor,BeanFactory的后处理器。
更多关于加密算法知识,可以关注公众号「SpringBoot」,回复关键词「256 或 md5」,查看文章:
《Spring Boot+Spring Security: MD5是加密算法吗?》

主要讲解了:加密算法的加密过程、解密过程、加密算法、对称加密、非对称加密。
1.2编码分析
接下来分析一下如何编码:
(1)加密算法准备工作,需要编写一个加密解密算法,这里使用AES对称加密算法。
(2)配置文件内容准备两个参数,一个是不加密的,一个是加密的数据。
(3)配置信息对应的Java类,一般的我们的配置信息都会存放到java类中,方便调用。
(4)使用Spring的扩展点BeanFactoryPostProcessor以及环境变量EnvironmentAware等获取到配置文件的配置内容的信息对信息解密。
二、敏感信息加密解密实战
接下来就是见证奇迹的时刻了,千万不要眨眼睛。
2.1 创建项目
使用idea创建一个项目,取名为spring-boot-decrypt,引入依赖:spring-boot-starter-web。
2.2加密算法工具类
编写一个加密算法工具类,这里使用AES对称加密算法进行加密解密:
package com.kfit.util;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.security.Key;
import java.security.SecureRandom;
public class DecryptUtil {
private static final String CHARSET = "utf-8";
private static final String ALGORITHM = "AES";
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
public static String aesEncrypt(String content, String key) {
if (content == null || key == null) {
return null;
}
Key secretKey = getKey(key);
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] p = content.getBytes(CHARSET);
byte[] result = cipher.doFinal(p);
BASE64Encoder encoder = new BASE64Encoder();
String encoded = encoder.encode(result);
return encoded;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String aesDecrypt(String content, String key) {
Key secretKey = getKey(key);
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
BASE64Decoder decoder = new BASE64Decoder();
byte[] c = decoder.decodeBuffer(content);
byte[] result = cipher.doFinal(c);
String plainText = new String(result, CHARSET);
return plainText;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Key getKey(String key) {
if (key == null) {
throw new NullPointerException("key不能为null");
}
try {
SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
secureRandom.setSeed(key.getBytes());
KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
generator.init(secureRandom);
return generator.generateKey();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
//对于密码 123456加密
String encryptPassword = DecryptUtil.aesEncrypt("123456", "wuqian@springboot");
//结果:NH6/q8SNo929e+bQ4g8dCA==
System.out.println(encryptPassword);
//对于newUserName进行解密
String originPassword = DecryptUtil.aesDecrypt(encryptPassword, "wuqian@springboot");
//结果:123456
System.out.println(originPassword);
}
}
说明:在这个类里有一个main方法,使用key为quqian@springboot对123456进行加密,结果为:NH6/q8SNo929e+bQ4g8dCA==。
2.3 配置文件内容
有了上面的加密信息,那配置文件就水到渠成了。在application.properties添加配置内容:
datasouce.username = root
datasouce.password = Enc[NH6/q8SNo929e+bQ4g8dCA==]
说明:这里对于加密的配置属性,使用Enc[作为前缀,使用]作为后缀,将加密信息进行包裹。
2.4 配置内容对应的java类
配置内容对应的java类:
package com.kfit.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
*
*
* @author 悟纤「公众号SpringBoot」
* @date 2022-09-15
* @slogan 大道至简 悟在天成
*/
@Component
public class MyDatasouce {
@Value("${datasouce.username}")
private String username;
@Value("${datasouce.password}")
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "MyDatasouce{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
2.5 对配置信息进行解密
接下来就是本文的重点了,也就是对加密信息进行解密,利用Spring的扩展点BeanFactoryPostProcessor(BeanFactory的后处理器)以及EnvironmentAware(用于获取Enviroment的一个扩展类,这个变量非常有用, 可以获得系统内的所有参数。),具体的示例代码如下:
package com.kfit.config;
import com.kfit.util.DecryptUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.Ordered;
import org.springframework.core.env.*;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* 对于加密的进行解密
*
* @author 悟纤「公众号SpringBoot」
* @date 2022-09-15
* @slogan 大道至简 悟在天成
*/
@Component
public class DecryptConfig implements EnvironmentAware, BeanFactoryPostProcessor, Ordered {
private ConfigurableEnvironment environment;
private String decryptPrefix = "Enc["; // 解密前缀标志 默认值
private String decryptSuffix = "]"; // 解密后缀标志 默认值
private String decryptKey = "wuqian@springboot"; // 解密秘钥 默认值
@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment)environment;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
System.out.println("敏感信息解密开始.....");
MutablePropertySources propSources = environment.getPropertySources();
StreamSupport.stream(propSources.spliterator(), false)
.filter(ps -> ps instanceof OriginTrackedMapPropertySource)
.collect(Collectors.toList())
.forEach(ps -> convertPropertySource(propSources,(PropertySource<Map>) ps));
System.out.println(&#34;敏感信息解密结束.....&#34;);
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* 解密相关属性
* @param ps : org.springframework.boot.env.OriginTrackedMapPropertySource
*/
private void convertPropertySource(MutablePropertySources propSources,PropertySource<Map> ps) {
//java.util.Collections$UnmodifiableMap
Map source = ps.getSource();
setDecryptProperties(source);
Map<Object, Object> newMap = new HashMap<>();
source.forEach((k,v) -> {
String value = String.valueOf(v);
if (!value.startsWith(decryptPrefix) || !value.endsWith(decryptSuffix)) {
newMap.put(k,value);
return;
}
String cipherText = value.replace(decryptPrefix, &#34;&#34;).replace(decryptSuffix, &#34;&#34;);
String clearText = DecryptUtil.aesDecrypt(cipherText, decryptKey);
//System.out.println(&#34;DecryptConfig.convertPropertySource-->&#34; + k +&#34;--&#34;+ clearText);
// 不能这么写了,因为UnmodifiableMap不可进行put/remove操作,否则会抛出异常UnsupportedOperationException
//source.put(k,clearText);
newMap.put(k,clearText);
});
propSources.replace(ps.getName(),new OriginTrackedMapPropertySource(ps.getName(),newMap));
}
/**
* 设置解密属性
* @param source
*/
private void setDecryptProperties(Map source) {
decryptPrefix = source.get(&#34;decrypt.prefix&#34;) == null ? decryptPrefix : String.valueOf(source.get(&#34;decrypt.prefix&#34;));
decryptSuffix = source.get(&#34;decrypt.suffix&#34;) == null ? decryptSuffix : String.valueOf(source.get(&#34;decrypt.suffix&#34;));
decryptKey = source.get(&#34;decrypt.key&#34;) == null ? decryptKey : String.valueOf(source.get(&#34;decrypt.key&#34;));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
}
说明:
(1)使用environment获取到所有的属性文件信息。
(2)使用java的lambda表达式进行刷选出满足条件的。
(3)对满足条件的属性文件进行替换处理。
这里要注意,早期是支持直接source.put(k,clearText)替换的,现在的版本不支持的,底层使用了Collections$UnmodifiableMap,因为UnmodifiableMap不可进行put/remove操作,否则会抛出异常UnsupportedOperationException。
更多关于EnvironmentAware知识,可以关注公众号「SpringBoot」,回复关键字「433」,查看文章:
《ApplicationContextAwareProcessor普通类获取Spring Bean》
更多关于BeanFactoryPostProcessor知识,可以关注公众号「SpringBoot」,回复关键字「428」,查看文章:
《SpringBoot/Spring扩展点系列之叱咤风云BeanFactoryPostProcessor - 第428篇》
更多关于Lambda知识,可以关注公众号「SpringBoot」,
回复关键字「lambda」,查看Lambda系列文章:
《Java8新特性:Lambda表达式:小试牛刀》
《Java8新特性:Lambda表达式:过关斩将:使用场景》
《Java8新特性:Lambda表达式: 摸摸里面》
回复关键字「lambda实战」,查看Lambda实战系列文章:
《你真的学会了Lambda表达式了吗?一篇让你学废了不香么 - 第417篇》
《当你的Stream遇上Lambda就爱上了,超级无敌酷酷!- 第418篇》
《java8+lambda+Stream api实战案例学彻底透学废 - 第419篇》
2.6 编写controller测试
编写一个controller打印下信息测试一下看看能不能得到原内容:
package com.kfit;
import com.kfit.config.MyDatasouce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
*
* @author 悟纤「公众号SpringBoot」
* @date 2022-09-15
* @slogan 大道至简 悟在天成
*/
@RestController
public class DemoController {
@Autowired
private MyDatasouce myDatasouce;
@RequestMapping(&#34;/test&#34;)
public MyDatasouce test(){
System.out.println(myDatasouce);
return myDatasouce;
}
}这时候就可以运行项目,访问如下地址:
http://127.0.0.1:8080/test
可以在控制台查看到信息:

说明我们成功的对加密的数据进行解密了。
三、jasypt框架源码
对于这个框架,我们就不细致展开分析了,直接看下核心的关键源码:
EnableEncryptablePropertiesBeanFactoryPostProcessor:

在这里也是实现了BeanFactoryPostProcessor扩展点,具体的处理逻辑交给了converter:

在这里的话,也是使用java的lambda表达式对数据进行了处理,最后交给了makeEncryptable():

在这里对于属性资源进行处理,返回了一个EncryptablePropertySource。
到这里,其实我们也是自己实现一个框架的,只是对于一个开源的框架要思考的点比较多,比如:可扩展性、代码的健壮性、安全性、性能等等。
更多关于jasypt的实战,可以关注公众号「SpringBoot」,回复关键字「431」,查看文章:
《SpringBoot配置文件内容加密,实现敏感信息保护 - 第431篇》
总结
最后来总结一下本节的重点内容:
(1)其一就是内容加密以及解密,加密算法文中使用的是AES。
(2)其二就是使用Enc[]包裹加密信息来区分配置文件里的内容哪些加密了,哪些未加密。
(3)其三使用到的扩展点是BeanFactory的后处理器BeanFactoryPostProcessor。
我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。
à悟空学院:https://t.cn/Rg3fKJD
学院中有Spring Boot相关的课程!点击「阅读原文」进行查看!
SpringBoot视频:http://t.cn/A6ZagYTi
SpringBoot交流平台:https://t.cn/R3QDhU0
SpringSecurity5.0视频:http://t.cn/A6ZadMBe
ShardingJDBC分库分表:http://t.cn/A6ZarrqS
分布式事务解决方案:http://t.cn/A6ZaBnIr
JVM内存模型调优实战:http://t.cn/A6wWMVqG
Spring入门到精通:https://t.cn/A6bFcDh4
大话设计模式之爱你:https://dwz.cn/wqO0MAy7 |
|