历史文章(文章累计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("敏感信息解密结束....."); } ? @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, "").replace(decryptSuffix, ""); String clearText = DecryptUtil.aesDecrypt(cipherText, decryptKey); //System.out.println("DecryptConfig.convertPropertySource-->" + k +"--"+ 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("decrypt.prefix") == null ? decryptPrefix : String.valueOf(source.get("decrypt.prefix")); decryptSuffix = source.get("decrypt.suffix") == null ? decryptSuffix : String.valueOf(source.get("decrypt.suffix")); decryptKey = source.get("decrypt.key") == null ? decryptKey : String.valueOf(source.get("decrypt.key")); } ? @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("/test") 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
|