当前位置: 首页 > 科技观察

springboot配置文件,隐私数据脱敏实践

时间:2023-03-18 17:10:47 科技观察

本文转载自微信公众号《程序员内政》,作者程序员内政。转载本文请联系程序员内电史公众号。大家好!我是小付~公司这几天一直在调查内部数据账号泄露事件。原因是一些可爱的实习生私自将源码用账号和密码上传到GitHub,导致核心数据泄露。孩子们还没有被社会打过,这种事情的后果可大可小。说到这里,我颇为感慨。以前被数据库删除的经历现在想想还是很难受。还把数据库账号的明文密码错提交到GitHub,然后test数据库被某大宝贝给删了,以后会记得把配置文件的内容全部加密。数据安全问题不容小觑。无论是工作还是生活,敏感数据都要脱敏。如果你对脱敏的概念不熟悉,可以看一下我之前写的一篇文章中各大厂商也在使用的6种数据脱敏方案,里面对脱敏做了简单的介绍。接下来分享工作中比较常见的两种数据脱敏方案。脱敏现场。配置脱敏为了实现配置脱敏,我使用了Java中的加解密工具Jasypt,它提供了两种脱敏方式:单密钥对称加密和非对称加密。单密钥对称加密:一个密钥加盐可以同时作为加解密内容的依据;非对称加密:只有两个密钥,公钥和私钥,可以用来加密和解密内容;以上两种加密方法使用起来都非常简单。下面以springboot集成的单密钥对称加密方式为例。com.github.ulisesbocchiojasypt-spring-boot-starter2.1.0<在/dependency>配置文件中添加关键配置项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用户名:xiaofupassword:ENC(mVTvp4IddqdaYGqPl9lCQbzBl3H)/:encryptor:password:程序员内部的东西(但不支持中文)秘钥是对安全性要求比较高的属性,所以一般不建议直接放在项目中,可以通过-D注入启动时的参数,或者放在配置中心,以免泄露。java-jar-Djasypt.encryptor.password=1123springboot-jasypt-2.3.3.RELEASE.jar预生成的加密值,可以通过代码中调用API@AutowiredprivateStringEncryptorstringEncryptor生成;publicvoidencrypt(Stringcontent){StringencryptStr=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-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()default"";}@Documented@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceEncryptMethod{Stringtype()defaultENCRYPT;}方面的实现也比较简单。加密条目并返回结果进行解密。为了阅读方便,这里只贴出部分代码。完整案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt@Slf4j@Aspect@ComponentpublicclassEncryptHandler{@AutowiredprivateStringEncryptorstringEncryptor;@Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)")publicvoidpointCut(){}@Around("pointCut()")publicObjectaround(ProceedingJoinPointjoinPoint){/***加密*/encrypt(joinPoint);/***Decrypt*/Objectdecrypt=decrypt(joinPoint);returndecrypt;}publicvoidencrypt(ProceedingJoinPointjoinPoint){try{Object[]objects=joinPoint.getArgs();if(objects.length!=0){for(Objecto:objects){if(oinstanceofString){encryptValue(o);}else{handler(o,ENCRYPT);}//TODO其他类型看实际情况,添加}}}catch(IllegalAccessExceptione){e.printStackTrace();}}publicObjectdecrypt(ProceedingJoinPointjoinPoint){Objectresult=null;try{Objectobj=joinPoint.proceed();if(obj!=null){if(objinstanceofString){decryptValue(obj);}else{result=handler(obj,DECRYPT);}//TODOrest自己输入看效果实际情况加}}catch(Throwablee){即。printStackTrace();}返回结果;}。.}在测试section注解的效果后,我们立即使用注解@EncryptField对字段mobile和address进行脱敏处理。@EncryptMethod@PostMapping(value="test")@ResponseBodypublicObjecttestEncrypt(@RequestBodyUserVouser,@EncryptFieldStringname){returninsertUser(user,name);}privateUserVoinsertUser(UserVouser,Stringname){System.out.println("加密数据:用户"+JSON.toJSONString(user));returnuser;}@DatapublicclassUserVoimplementsSerializable{privateLonguserId;@EncryptFieldprivateStringmobile;@EncryptFieldprivateStringaddress;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-nds/Springboot-Notebook/tree/master/springboot-jasyptPBE算法下面说说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,key);//密文头(salt值)byte[]params=cipherEncrypt.getParameters().getEncoded();//调用底层加密byte[]encryptedMessage=cipherEncrypt.doFinal(message);//组装最终的密文内容并分配内存(salt值+密文)returnByteBuffer.allocate(1+params.length+encryptedMessage.length).put((byte)params.length).put(params).put(encryptedMessage).array();}自生成随机盐值默认使用r,相同内容的加密内容每次都不一样。那么解密的时候应该怎么对应呢?查看上面的源码,发现最终的密文由两部分组成。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);//构建header盐值密码参数AlgorithmParametersalgorithmParameters=AlgorithmParameters=AlgorithmParameters=AlgorithmgetInstance(algorithm1);algorithmParameters.init(params);//构建加密器并调用底层算法(信息);}解密