乐趣区

关于java:Springboot-配置文件隐私数据脱敏的最佳实践原理源码

大家好!我是小富~

这几天公司在排查外部数据账号透露,起因是发现某些实习生小可爱竟然连带着账号、明码将源码私传到 GitHub 上,导致外围数据外漏,孩子还是没挨过社会毒打,这种事的结果可大可小。

说起这个我是比拟有感触的,之前我 TM 被删库的经验,到当初想起来心里还好受,我也是把数据库账号明文明码误提交到GitHub,而后被哪个大宝贝给我测试库删了,后边我长忘性了把配置文件内容都加密了,数据安全问题真的不容小觑,不论工作汇还是生存,敏感数据肯定要做脱敏解决。

如果对脱敏概念不相熟,能够看一下我之前写过的一篇大厂也在用的 6 种数据脱敏计划,里边对脱敏做了简略的形容,接下来分享工作中两个比拟常见的脱敏场景。

配置脱敏

实现配置的脱敏我应用了 Java 的一个加解密工具 Jasypt,它提供了 单密钥对称加密 非对称加密 两种脱敏形式。

单密钥对称加密:一个密钥加盐,能够同时用作内容的加密和解密根据;

非对称加密:应用公钥和私钥两个密钥,才能够对内容加密和解密;

以上两种加密形式应用都非常简单,咱们以 springboot 集成单密钥对称加密形式做示例。

首先引入jasypt-spring-boot-starter jar

 <!-- 配置文件加密 -->
 <dependency>
     <groupId>com.github.ulisesbocchio</groupId>
     <artifactId>jasypt-spring-boot-starter</artifactId>
     <version>2.1.0</version>
 </dependency>

配置文件退出秘钥配置项 jasypt.encryptor.password,并将须要脱敏的value 值替换成事后通过加密的内容ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)

这个格局咱们是能够随便定义的,比方想要 abc[mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l] 格局,只有配置前缀和后缀即可。

jasypt:
  encryptor:
    property:
      prefix: "abc["
      suffix: "]"

ENC(XXX)格局次要为了便于辨认该值是否须要解密,如不依照该格局配置,在加载配置项的时候 jasypt 将放弃原值,不进行解密。

spring:
  datasource:
    url: jdbc:mysql://1.2.3.4:3306/xiaofu?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: xiaofu
    password: ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)

# 秘钥
jasypt:
  encryptor:
    password: 程序员内点事(然而不反对中文)

秘钥是个安全性要求比拟高的属性,所以个别不倡议间接放在我的项目内,能够通过启动时 -D 参数注入,或者放在配置核心,防止泄露。

java -jar -Djasypt.encryptor.password=1123  springboot-jasypt-2.3.3.RELEASE.jar

事后生成的加密值,能够通过代码内调用 API 生成

@Autowired
private StringEncryptor stringEncryptor;

public void encrypt(String content) {String encryptStr = stringEncryptor.encrypt(content);
    System.out.println("加密后的内容:" + encryptStr);
}

或者通过如下 Java 命令生成,几个参数 D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar 为 jasypt 外围 jar 包,input待加密文本,password秘钥,algorithm为应用的加密算法。

java -cp  D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password=xiaofu  algorithm=PBEWithMD5AndDES

一顿操作后如果还能失常启动,阐明配置文件脱敏就没问题了。

敏感字段脱敏

生产环境用户的隐衷数据,比方手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入咱们零碎时就要实时的脱敏解决。

用户数据进入零碎,脱敏解决后长久化到数据库,用户查问数据时还要进行反向解密。这种场景个别须要全局解决,那么用 AOP 切面来实现在适宜不过了。

首先自定义两个注解 @EncryptField@EncryptMethod 别离用在字段属性和办法上,实现思路很简略,只有办法上利用到 @EncryptMethod 注解,则查看入参字段是否标注 @EncryptField 注解,有则将对应字段内容加密。

@Documented
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {String[] value() default "";}
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {String type() default ENCRYPT;
}

切面的实现也比较简单,对入加入密,返回后果解密。为了不便浏览这里就只贴出局部代码,残缺案例 Github 地址:https://github.com/chengxy-nd…

@Slf4j
@Aspect
@Component
public class EncryptHandler {

    @Autowired
    private StringEncryptor stringEncryptor;

    @Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)")
    public void pointCut() {}

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        /**
         * 加密
         */
        encrypt(joinPoint);
        /**
         * 解密
         */
        Object decrypt = decrypt(joinPoint);
        return decrypt;
    }

    public void encrypt(ProceedingJoinPoint joinPoint) {

        try {Object[] objects = joinPoint.getArgs();
            if (objects.length != 0) {for (Object o : objects) {if (o instanceof String) {encryptValue(o);
                    } else {handler(o, ENCRYPT);
                    }
                    //TODO 其余类型本人看理论状况加
                }
            }
        } catch (IllegalAccessException e) {e.printStackTrace();
        }
    }

    public Object decrypt(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {Object obj = joinPoint.proceed();
            if (obj != null) {if (obj instanceof String) {decryptValue(obj);
                } else {result = handler(obj, DECRYPT);
                }
                //TODO 其余类型本人看理论状况加
            }
        } catch (Throwable e) {e.printStackTrace();
        }
        return result;
    }。。。}

紧接着测试一下切面注解的成果,咱们对字段 mobileaddress 加上注解 @EncryptField 做脱敏解决。

@EncryptMethod
@PostMapping(value = "test")
@ResponseBody
public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) {return insertUser(user, name);
}

private UserVo insertUser(UserVo user, String name) {System.out.println("加密后的数据:user" + JSON.toJSONString(user));
    return user;
}

@Data
public class UserVo implements Serializable {

    private Long userId;

    @EncryptField
    private String mobile;

    @EncryptField
    private String address;

    private String age;
}

申请这个接口,看到参数被胜利加密,而返回给用户的数据仍然是脱敏前的数据,合乎咱们的预期,那到这简略的脱敏实现就完事了。

知其然知其所以然

Jasypt工具尽管简略好用,但作为程序员咱们不能仅满足于纯熟应用,底层实现原理还是有必要理解下的,这对后续调试 bug、二次开发扩大性能很重要。

集体认为 Jasypt 配置文件脱敏的原理很简略,无非就是在具体应用配置信息之前,先拦挡获取配置的操作,将对应的加密配置解密后再应用。

具体是不是如此咱们简略看下源码的实现,既然是以 springboot 形式集成,那么就先从 jasypt-spring-boot-starter 源码开始动手。

starter代码很少,次要的工作就是通过 SPI 机制注册服务和 @Import 注解来注入需前置解决的类JasyptSpringBootAutoConfiguration

在前置加载类 EnableEncryptablePropertiesConfiguration 中注册了一个外围解决类EnableEncryptablePropertiesBeanFactoryPostProcessor

它的结构器有两个参数,ConfigurableEnvironment用来获取所有配属信息,EncryptablePropertySourceConverter对配置信息做解析解决。

顺藤摸瓜发现具体负责解密的解决类 EncryptablePropertySourceWrapper,它通过对Spring 属性治理类 PropertySource<T> 做拓展,重写了 getProperty(String name) 办法,在获取配置时,但凡指定格局如ENC(x) 包裹的值全副解密解决。

既然晓得了原理那么后续咱们二次开发,比方:切换加密算法或者实现本人的脱敏工具就容易的多了。

案例 Github 地址:https://github.com/chengxy-nd…

PBE 算法

再来聊一下 Jasypt 中用的加密算法,其实它是在 JDK 的 JCE.jar 包根底上做了封装,实质上还是用的 JDK 提供的算法,默认应用的是 PBE 算法PBEWITHMD5ANDDES,看到这个算法命名很有意思,段个句看看,PBE、WITH、MD5、AND、DES 如同有点故事,持续看。

PBE算法(Password Based Encryption,基于口令(明码)的加密)是一种基于口令的加密算法,其特点在于口令是由用户本人把握,在加上随机数多重加密等办法保证数据的安全性。

PBE 算法实质上并没有真正构建新的加密、解密算法,而是对咱们已知的算法做了包装。比方:罕用的音讯摘要算法 MD5SHA算法,对称加密算法 DESRC2 等,而 PBE 算法就是将这些算法进行正当组合,这也响应上前边算法的名字。

既然 PBE 算法应用咱们较为罕用的对称加密算法,那就会波及密钥的问题。但它自身又没有钥的概念,只有口令明码,密钥则是口令通过加密算法计算得来的。

口令自身并不会很长,所以不能用来代替密钥,只用口令很容易通过穷举攻击方式破译,这时候就得加点 了。

盐通常会是一些随机信息,比方随机数、工夫戳,将盐附加在口令上,通过算法计算加大破译的难度。

源码里的猫腻

简略理解 PBE 算法,回过头看看 Jasypt 源码是如何实现加解密的。

在加密的时候首先实例化秘钥工厂 SecretKeyFactory,生成八位盐值,默认应用的jasypt.encryptor.RandomSaltGenerator 生成器。

public byte[] encrypt(byte[] message) {
    // 依据指定算法,初始化秘钥工厂
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
    // 盐值生成器,只选八位
    byte[] salt = saltGenerator.generateSalt(8);
    // 
    final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations);
    // 盐值、口令生成秘钥
    SecretKey key = factory.generateSecret(keySpec);

    // 构建加密器
    final Cipher cipherEncrypt = Cipher.getInstance(algorithm1);
    cipherEncrypt.init(Cipher.ENCRYPT_MODE, key);
    // 密文头部(盐值)byte[] params = cipherEncrypt.getParameters().getEncoded();

    // 调用底层实现加密
    byte[] encryptedMessage = cipherEncrypt.doFinal(message);

    // 组装最终密文内容并分配内存(盐值 + 密文)return ByteBuffer
            .allocate(1 + params.length + encryptedMessage.length)
            .put((byte) params.length)
            .put(params)
            .put(encryptedMessage)
            .array();}

因为默认应用的是随机盐值生成器,导致雷同 内容每次加密后的内容都是不同的

那么解密时该怎么对应上呢?

看上边的源码发现,最终的加密文本是由两局部组成的,params音讯头里边蕴含口令和随机生成的盐值,encryptedMessage密文。

而在解密时会依据密文 encryptedMessage 的内容拆解出 params 内容解析出盐值和口令,在调用 JDK 底层算法解密出理论内容。

@Override
@SneakyThrows
public byte[] decrypt(byte[] encryptedMessage) {
    // 获取密文头部内容
    int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]);
    // 获取密文内容
    int messageLength = encryptedMessage.length - paramsLength - 1;
    byte[] params = new byte[paramsLength];
    byte[] message = new byte[messageLength];
    System.arraycopy(encryptedMessage, 1, params, 0, paramsLength);
    System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength);

    // 初始化秘钥工厂
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
    final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
    SecretKey key = factory.generateSecret(keySpec);

    // 构建头部盐值口令参数
    AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(algorithm1);
    algorithmParameters.init(params);

    // 构建加密器,调用底层算法
    final Cipher cipherDecrypt = Cipher.getInstance(algorithm1);
    cipherDecrypt.init(
            Cipher.DECRYPT_MODE,
            key,
            algorithmParameters
    );
    return cipherDecrypt.doFinal(message);
}

我是小富,下期见~

整顿了几百本各类技术电子书,有须要的同学自取。技术群快满了,想进的同学能够加我好友,和大佬们一起吹吹技术。

电子书地址

集体公众号:程序员内点事,欢送交换

退出移动版