大家好!我是小富~

这几天公司在排查外部数据账号透露,起因是发现某些实习生小可爱竟然连带着账号、明码将源码私传到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生成

@Autowiredprivate 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@Componentpublic 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")@ResponseBodypublic 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;}@Datapublic 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@SneakyThrowspublic 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);}

我是小富,下期见~

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

电子书地址

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