当前位置: 首页 > 后端技术 > Java

Springboot配置文件与隐私数据脱敏最佳实践(原理+源码)

时间:2023-04-01 16:59:25 Java

大家好!我是小付~公司这几天一直在调查内部数据账号泄露的问题。原因是有萌萌哒实习生甚至私自将源码用账号和密码上传到GitHub,导致核心数据泄露。孩子们还没有活下来。社会殴打,这种事情的后果可大可小。说到这里,我颇为感慨。以前被数据库删除的经历现在想想还是很难受。还把数据库账号的明文密码错提交到GitHub,然后test数据库被某大宝贝给删了,以后会记得把配置文件的内容全部加密。数据安全问题不容小觑。无论是工作还是生活,敏感数据都要脱敏。如果你对脱敏的概念不熟悉,可以看一下我之前写的一篇文章中各大厂商也在使用的6种数据脱敏方案,里面对脱敏做了简单的介绍。接下来分享工作中比较常见的两种数据脱敏方案。脱敏现场。配置脱敏为了实现配置脱敏,我使用了Java中的加解密工具Jasypt,它提供了两种脱敏方式:单密钥对称加密和非对称加密。单密钥对称加密:一个密钥加盐可以同时作为加解密内容的依据;非对称加密:只有两个密钥,公钥和私钥,可以用来加密和解密内容;以上两种加密方法使用起来都非常简单。下面以springboot集成的单密钥对称加密方式为例。首先引入jasypt-spring-boot-starterjarcom.github.ulisesbocchiojasypt-spring-boot-starter2.1.0配置文件增加关键配置项jasypt.encryptor.password,将需要脱敏的值替换为预加密内容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&zeoDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai用户名:xiaofu密码:ENC(mVTvp4IddqdaYGqPl9lCQbzB6l3H)#b秘钥jasypt:encryptor:password:程序员内部事务(但不支持中文)秘钥是对安全性要求比较高的属性,所以一般不建议直接放在项目中,可以通过注入启动时的-D参数,或者放在配置中心,以免泄露。java-jar-Djasypt.encryptor.password=1123springboot-jasypt-2.3.3.RELEASE.jar预生成的加密值可以通过调用代码中的API生成@AutowiredprivateStringEncryptorstringEncryptor;publicvoidencrypt(Stringcontent){字符串encryptStr=stringEncryptor.encrypt(内容);System.out.println("加密内容:"+encryptStr);}或者通过下面的Java命令生成,几个参数D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar是jasypt核心jar包,输入要加密的文本,密码key,algorithm是使用的加密算法。java-cpD:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jarorg.jasypt.intf.cli.JasyptPBEStringEncryptionCLIinput="root"password=xiaofualgorithm=PBEWithMD5AndDES如果还能启动正常,说明配置文件脱敏没问题。敏感字段脱敏生产环境中用户的隐私数据,比如手机号,身份证,或者一些账户配置信息,入库时需要进行不落地脱敏,即入库时实时脱敏进入我们的系统。用户数据进入系统,脱敏持久化到数据库,在用户查询数据时进行反向解密。这种场景一般需要全局处理,不适合用AOP切面来实现。首先自定义两个注解@EncryptField和@EncryptMethod,分别用在字段属性和方法上。实现思路很简单。只要在方法上应用了@EncryptMethod注解,检查输入字段是否有@EncryptField注解标注。如果有,则对应的字段为Contentencryption。@Documented@Target({ElementType.FIELD,ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public@interfaceEncryptField{String[]value()默认"";}@Documented@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceEncryptMethod{Stringtype()defaultENCRYPT;}方面的实现也比较简单。加密条目并返回结果进行解密。为了阅读方便,这里只贴出部分代码。完整案例的Github地址:https://github.com/chengxy-nd...@Slf4j@Aspect@ComponentpublicclassEncryptHandler{@AutowiredprivateStringEncryptorstringEncryptor;@Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)")publicvoidpointCut(){}@Around("pointCut()")publicObjectaround(ProceedingJoinPointjoinPoint){/***encryption*/encrypt(连接点);/***解密*/Objectdecrypt=decrypt(joinPoint);返回解密;}publicvoidencrypt(ProceedingJoinPointjoinPoint){try{Object[]objects=joinPoint.getArgs();if(objects.length!=0){for(Objecto:objects){if(oinstanceofString){encryptValue(o);}else{处理程序(o,加密);}//TODO其他类型视实际情况而定}}}catch(IllegalAccessExceptione){e.printStackTrace();}}publicObjectdecrypt(ProceedingJoinPointjoinPoint){Objectresult=null;尝试{对象obj=joinPoint。继续();if(obj!=null){if(objinstanceofString){decryptValue(obj);}else{result=handler(obj,DECRYPT);}//TODO其他类型看实际情况加上}}catch(Throwablee){e.printStackTrace();}返回结果;}。.}在测试section注解的效果后,我们立即使用注解@EncryptField对字段mobile和address进行脱敏处理。@EncryptMethod@PostMapping(value="test")@ResponseBodypublicObjecttestEncrypt(@RequestBodyUserVouser,@EncryptFieldStringname){returninsertUser(user,name);}privateUserVoinsertUser(UserVouser,Stringname){System.out.println("加密数据:用户"+JSON.toJSONString(user));返回用户;}@DatapublicclassUserVoimplementsSerializable{privateLonguserId;@EncryptField私有字符串移动;@EncryptField私有字符串地址;privateStringage;}请求这个接口,看到参数加密成功,返回给用户的数据还是脱敏前的数据,符合我们的预期,那么简单的脱敏实现就结束了。知其然,知其为何简单易用,但作为程序员,我们不能仅仅满足于熟练使用它。需要了解底层实现原理,这对于后续调试bug和扩展功能的二次开发非常重要。个人认为Jasypt配置文件的脱敏原理很简单。无非就是拦截在使用配置信息前获取配置的操作,在使用前将对应的加密配置解密。是这样吗?下面简单看一下源码的实现。既然是以springboot的方式集成的,那就从jasypt-spring-boot-starter的源码入手吧。起步代码很小,主要工作是通过SPI机制注册服务和@Import注解注入需要预处理的类JasyptSpringBootAutoConfiguration。在预加载类EnableEncryptablePropertiesConfiguration中注册了一个核心处理类EnableEncryptablePropertiesBeanFactoryPostProcessor。它的构造函数有两个参数,ConfigurableEnvironment用于获取所有附件信息,EncryptablePropertySourceConverter解析配置信息。顺藤摸瓜发现,EncryptablePropertySourceWrapper是负责解密的处理类。它扩展了Spring属性管理类PropertySource,并重写了getProperty(Stringname)方法。获取配置时,任何以指定格式包装的值,如ENC(x)全部解密。知道了原理之后,我们进行二次开发,比如切换加密算法,或者实现自己的脱敏工具等,就会容易很多。案例Github地址:https://github.com/chengxy-nd...PBE算法下面说说Jasypt中使用的加密算法。其实就是在JDK的JCE.jar包的基础上进行了封装。本质上,它仍然使用JDK提供的算法,默认使用PBE算法PBEWITHMD5ANDDES。看到这个算法的名字很有意思。让我们来看看吧。PBE、WITH、MD5、AND、DES好像都有故事,继续往下看。PBE算法(PasswordBasedEncryption,基于密码(密码)的加密)是一种基于密码的加密算法,其特点是密码由用户自己掌握,并加入随机数等多种加密方式,确保数据安全安全。PBE算法本质上并没有真正构造出新的加解密算法,而是对已知算法进行了包装。例如:常用的消息摘要算法MD5和SHA算法,对称加密算法DES、RC2等,而PBE算法就是这些算法的合理组合,这也呼应了前面算法的名称。由于PBE算法使用的是我们比较常用的对称加密算法,所以会涉及到密钥的问题。但它本身并没有密钥的概念,只有密码和密码,而密钥是由密码通过加密算法计算出来的。密码本身不是很长,所以不能代替密钥使用。仅使用密码,穷举攻击很容易破译。这时候就得加点盐了。Salt通常是一些随机信息,比如随机数和时间戳。给密码加盐会增加算法计算破译的难度。源码中的技巧简单了解一下PBE算法,回过头来看看Jasypt源码是如何实现加解密的。加密时,首先实例化秘钥工厂SecretKeyFactory生成八位salt值,默认使用jasypt.encryptor.RandomSaltGenerator生成器。publicbyte[]encrypt(byte[]message){//根据指定的算法,初始化秘钥工厂finalSecretKeyFactoryfactory=SecretKeyFactory.getInstance(algorithm1);//盐值生成器,只选择八个byte[]salt=saltGenerator.generateSalt(8);//finalPBEKeySpeckeySpec=newPBEKeySpec(password.toCharArray(),salt,iterations);//盐值、密码生成秘钥SecretKeykey=factory.generateSecret(keySpec);//构建一个加密器finalCiphercipherEncrypt=Cipher.getInstance(algorithm1);cipherEncrypt.init(Cipher.ENCRYPT_MODE,密钥);//密文头(盐值)byte[]params=cipherEncrypt.getParameters().getEncoded();//调用底层实现加密byte[]encryptedMessage=cipherEncrypt.doFinal(message);//组装最终的密文内容并分配内存(盐值+密文)).array();}由于默认使用了随机盐值生成器,所以每次加密相同内容的内容都不一样。那么解密的时候怎么处理呢?查看上面的源码,发现最终的密文是由两部分组成的。params消息头包含密码和随机生成的盐值,encryptedMessage密文。解密时,会根据encryptedMessage的内容,对params的内容进行拆解,解析出salt值和password,然后调用JDK的底层算法,对实际内容进行解密。@Override@Sn??eakyThrowspublicbyte[]decrypt(byte[]encryptedMessage){//获取密文头内容intparamsLength=Byte.toUnsignedInt(encryptedMessage[0]);//获取密文内容intmessageLength=encryptedMessage.length-paramsLength-1;byte[]params=newbyte[paramsLength];byte[]message=newbyte[messageLength];System.arraycopy(encryptedMessage,1,params,0,paramsLength);System.arraycopy(encryptedMessage,paramsLength+1,message,0,messageLength);//初始化密钥工厂finalSecretKeyFactoryfactory=SecretKeyFactory.getInstance(algorithm1);finalPBEKeySpeckeySpec=newPBEKeySpec(password.toCharArray());SecretKeykey=factory.generateSecret(keySpec);//构建标头盐值密码参数AlgorithmParametersalgorithmParameters=AlgorithmParameters.getInstance(algorithm1);algorithmParameters.init(参数);//构建加密器并调用底层算法finalCiphercipherDecrypt=Cipher.getInstance(algorithm1);cipherDecrypt.init(密码。DECRYPT_MODE,密钥,算法参数);returncipherDecrypt.doFinal(message);}我是小付,下期见~我整理了上百本各种类型的技术电子书。有需要的同学可以自己接技术组。技术群快满员了,想进的同学可以加我为好友,和大佬一起吹吹技术。电子书地址个人公众号:程序员,欢迎交流