Springboot 配置文件、隐私数据脱敏实践

开发 架构
如果对脱敏概念不熟悉,可以看一下我之前写过的一篇大厂也在用的6种数据脱敏方案,里边对脱敏做了简单的描述,接下来分享工作中两个比较常见的脱敏场景。

[[414887]]

本文转载自微信公众号「程序员内点事」,作者程序员内点事。转载本文请联系程序员内点事公众号。

大家好!我是小富~

这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小。

说起这个我是比较有感触的,之前我TM被删库的经历,到现在想起来心里还难受,我也是把数据库账号明文密码误提交到GitHub,然后被哪个大宝贝给我测试库删了,后边我长记性了把配置文件内容都加密了,数据安全问题真的不容小觑,不管工作汇还是生活,敏感数据一定要做脱敏处理。

如果对脱敏概念不熟悉,可以看一下我之前写过的一篇大厂也在用的6种数据脱敏方案,里边对脱敏做了简单的描述,接下来分享工作中两个比较常见的脱敏场景。

配置脱敏

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

单密钥对称加密:一个密钥加盐,可以同时用作内容的加密和解密依据;

非对称加密:使用公钥和私钥两个密钥,才可以对内容加密和解密;

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

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

配置文件加入秘钥配置项jasypt.encryptor.password,并将需要脱敏的value值替换成预先经过加密的内容ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)。

这个格式我们是可以随意定义的,比如想要abc[mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l]格式,只要配置前缀和后缀即可。

  1. jasypt: 
  2.   encryptor: 
  3.     property: 
  4.       prefix: "abc[" 
  5.       suffix: "]" 

ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候jasypt将保持原值,不进行解密。

  1. spring: 
  2.   datasource: 
  3.     url: jdbc:mysql://1.2.3.4:3306/xiaofu?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai 
  4.     username: xiaofu 
  5.     password: ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l) 
  6.  
  7. # 秘钥 
  8. jasypt: 
  9.   encryptor: 
  10.     password: 程序员内点事(然而不支持中文) 

秘钥是个安全性要求比较高的属性,所以一般不建议直接放在项目内,可以通过启动时-D参数注入,或者放在配置中心,避免泄露。

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

预先生成的加密值,可以通过代码内调用API生成

  1. @Autowired 
  2. private StringEncryptor stringEncryptor; 
  3.  
  4. public void encrypt(String content) { 
  5.     String encryptStr = stringEncryptor.encrypt(content); 
  6.     System.out.println("加密后的内容:" + encryptStr); 

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

  1. 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注解,有则将对应字段内容加密。

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

切面的实现也比较简单,对入参加密,返回结果解密。为了方便阅读这里就只贴出部分代码,完整案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt

  1. @Slf4j 
  2. @Aspect 
  3. @Component 
  4. public class EncryptHandler { 
  5.  
  6.     @Autowired 
  7.     private StringEncryptor stringEncryptor; 
  8.  
  9.     @Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)"
  10.     public void pointCut() { 
  11.     } 
  12.  
  13.     @Around("pointCut()"
  14.     public Object around(ProceedingJoinPoint joinPoint) { 
  15.         /** 
  16.          * 加密 
  17.          */ 
  18.         encrypt(joinPoint); 
  19.         /** 
  20.          * 解密 
  21.          */ 
  22.         Object decrypt = decrypt(joinPoint); 
  23.         return decrypt; 
  24.     } 
  25.  
  26.     public void encrypt(ProceedingJoinPoint joinPoint) { 
  27.  
  28.         try { 
  29.             Object[] objects = joinPoint.getArgs(); 
  30.             if (objects.length != 0) { 
  31.                 for (Object o : objects) { 
  32.                     if (o instanceof String) { 
  33.                         encryptValue(o); 
  34.                     } else { 
  35.                         handler(o, ENCRYPT); 
  36.                     } 
  37.                     //TODO 其余类型自己看实际情况加 
  38.                 } 
  39.             } 
  40.         } catch (IllegalAccessException e) { 
  41.             e.printStackTrace(); 
  42.         } 
  43.     } 
  44.  
  45.     public Object decrypt(ProceedingJoinPoint joinPoint) { 
  46.         Object result = null
  47.         try { 
  48.             Object obj = joinPoint.proceed(); 
  49.             if (obj != null) { 
  50.                 if (obj instanceof String) { 
  51.                     decryptValue(obj); 
  52.                 } else { 
  53.                     result = handler(obj, DECRYPT); 
  54.                 } 
  55.                 //TODO 其余类型自己看实际情况加 
  56.             } 
  57.         } catch (Throwable e) { 
  58.             e.printStackTrace(); 
  59.         } 
  60.         return result; 
  61.     } 
  62.     。。。 

紧接着测试一下切面注解的效果,我们对字段mobile、address加上注解@EncryptField做脱敏处理。

  1. @EncryptMethod 
  2. @PostMapping(value = "test"
  3. @ResponseBody 
  4. public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) { 
  5.  
  6.     return insertUser(username); 
  7.  
  8. private UserVo insertUser(UserVo user, String name) { 
  9.     System.out.println("加密后的数据:user" + JSON.toJSONString(user)); 
  10.     return user
  11.  
  12. @Data 
  13. public class UserVo implements Serializable { 
  14.  
  15.     private Long userId; 
  16.  
  17.     @EncryptField 
  18.     private String mobile; 
  19.  
  20.     @EncryptField 
  21.     private String address; 
  22.  
  23.     private String age; 

请求这个接口,看到参数被成功加密,而返回给用户的数据依然是脱敏前的数据,符合我们的预期,那到这简单的脱敏实现就完事了。

知其然知其所以然

Jasypt工具虽然简单好用,但作为程序员我们不能仅满足于熟练使用,底层实现原理还是有必要了解下的,这对后续调试bug、二次开发扩展功能很重要。

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

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

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

在前置加载类EnableEncryptablePropertiesConfiguration中注册了一个核心处理类EnableEncryptablePropertiesBeanFactoryPostProcessor。

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

顺藤摸瓜发现具体负责解密的处理类EncryptablePropertySourceWrapper,它通过对Spring属性管理类PropertySource做拓展,重写了getProperty(String name)方法,在获取配置时,凡是指定格式如ENC(x) 包裹的值全部解密处理。

既然知道了原理那么后续我们二次开发,比如:切换加密算法或者实现自己的脱敏工具就容易的多了。

案例Github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt

PBE算法

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

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

PBE算法本质上并没有真正构建新的加密、解密算法,而是对我们已知的算法做了包装。比如:常用的消息摘要算法MD5和SHA算法,对称加密算法DES、RC2等,而PBE算法就是将这些算法进行合理组合,这也呼应上前边算法的名字。

既然PBE算法使用我们较为常用的对称加密算法,那就会涉及密钥的问题。但它本身又没有钥的概念,只有口令密码,密钥则是口令经过加密算法计算得来的。

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

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

源码里的猫腻

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

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

  1. public byte[] encrypt(byte[] message) { 
  2.     // 根据指定算法,初始化秘钥工厂 
  3.     final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1); 
  4.     // 盐值生成器,只选八位 
  5.     byte[] salt = saltGenerator.generateSalt(8); 
  6.     //  
  7.     final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations); 
  8.     // 盐值、口令生成秘钥 
  9.     SecretKey key = factory.generateSecret(keySpec); 
  10.  
  11.     // 构建加密器 
  12.     final Cipher cipherEncrypt = Cipher.getInstance(algorithm1); 
  13.     cipherEncrypt.init(Cipher.ENCRYPT_MODE, key); 
  14.     // 密文头部(盐值) 
  15.     byte[] params = cipherEncrypt.getParameters().getEncoded(); 
  16.  
  17.     // 调用底层实现加密 
  18.     byte[] encryptedMessage = cipherEncrypt.doFinal(message); 
  19.  
  20.     // 组装最终密文内容并分配内存(盐值+密文) 
  21.     return ByteBuffer 
  22.             .allocate(1 + params.length + encryptedMessage.length) 
  23.             .put((byte) params.length) 
  24.             .put(params) 
  25.             .put(encryptedMessage) 
  26.             .array(); 

由于默认使用的是随机盐值生成器,导致相同内容每次加密后的内容都是不同的。

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

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

加密

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

  1. @Override 
  2. @SneakyThrows 
  3. public byte[] decrypt(byte[] encryptedMessage) { 
  4.     // 获取密文头部内容 
  5.     int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]); 
  6.     // 获取密文内容 
  7.     int messageLength = encryptedMessage.length - paramsLength - 1; 
  8.     byte[] params = new byte[paramsLength]; 
  9.     byte[] message = new byte[messageLength]; 
  10.     System.arraycopy(encryptedMessage, 1, params, 0, paramsLength); 
  11.     System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength); 
  12.  
  13.     // 初始化秘钥工厂 
  14.     final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1); 
  15.     final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); 
  16.     SecretKey key = factory.generateSecret(keySpec); 
  17.  
  18.     // 构建头部盐值口令参数 
  19.     AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(algorithm1); 
  20.     algorithmParameters.init(params); 
  21.  
  22.     // 构建加密器,调用底层算法 
  23.     final Cipher cipherDecrypt = Cipher.getInstance(algorithm1); 
  24.     cipherDecrypt.init( 
  25.             Cipher.DECRYPT_MODE, 
  26.             key
  27.             algorithmParameters 
  28.     ); 
  29.     return cipherDecrypt.doFinal(message); 

解密

 

责任编辑:武晓燕 来源: 程序员内点事
相关推荐

2024-02-05 13:39:00

隐私数据脱敏

2022-06-23 08:42:08

配置加密解密

2021-04-01 10:23:45

SpringBootbootstrapapplication

2016-11-22 09:40:08

2011-01-19 14:00:21

2011-01-13 16:27:26

Linux配置文件

2022-08-17 07:06:14

SpringBoot配置@Value

2022-09-23 07:33:04

Springzookeeper配置

2010-12-27 14:59:31

Outlook 配置文

2010-12-28 16:35:32

Outlook 配置文

2011-03-28 15:52:16

Nagios配置文件

2011-04-01 16:30:49

Nagios

2009-07-09 15:55:18

WebWork配置文件

2011-03-28 09:07:26

Nagios配置文件

2011-03-03 09:14:38

PureFTPd

2024-03-14 08:05:30

2010-05-05 16:14:09

Unix管理

2010-08-05 14:36:07

NFS服务

2011-03-25 17:13:37

Nagios配置文件

2009-09-22 10:23:15

Hibernate配置
点赞
收藏

51CTO技术栈公众号