用户名  找回密码
 立即注册
注册 登录
×
热搜: 活动 交友 discuz
查看: 101|回复: 2

利用Spring扩展点对敏感信息加密解密,一文集齐n多知识点 ...

[复制链接]

1

主题

5

帖子

6

积分

新手上路

Rank: 1

积分
6
发表于 2022-11-30 14:08:00 | 显示全部楼层 |阅读模式


历史文章(文章累计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
回复

举报

1

主题

5

帖子

11

积分

新手上路

Rank: 1

积分
11
发表于 2025-3-12 05:31:34 | 显示全部楼层
垃圾内容,路过为证。
回复

举报

1

主题

4

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2025-3-25 16:53:44 | 显示全部楼层
顶起顶起顶起
回复

举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋| 黑客通

GMT+8, 2025-4-8 16:17 , Processed in 0.111892 second(s), 22 queries .

Powered by Discuz! X3.4

Copyright © 2020, LianLian.

快速回复 返回顶部 返回列表