大家好!我是小富~

这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到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-nds/Springboot-Notebook/tree/master/springboot-jasypt

@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-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算法本质上并没有真正构建新的加密、解密算法,而是对我们已知的算法做了包装。比如:常用的消息摘要算法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);
}

我是小富,下期见~

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

电子书地址

个人公众号: 程序员内点事,欢迎交流

Springboot 配置文件、隐私数据脱敏的最佳实践(原理+源码)的更多相关文章

  1. Salesforce 大量数据部署的最佳实践

    本文参考自官方文档.原文链接 大量数据部署对Salesforce的影响 当用户需要在Salesforce中部署大量数据的时候,部署的过程往往会变慢.这时就需要架构师或开发者设计出更好的过程来提高大量数 ...

  2. 【翻译】ScyllaDB数据建模的最佳实践

    文章翻译自Scylla官方文档:https://www.scylladb.com/2019/08/20/best-practices-for-data-modeling/ 转载请注明出处:https: ...

  3. Atitit.angular.js 使用最佳实践 原理与常见问题解决与列表显示案例 attilax总结

    Atitit.angular.js 使用最佳实践 原理与常见问题解决与列表显示案例 attilax总结 1. 本文范围 1 2. Angular的优点 1 2.1. 双向数据绑定 1 2.2. dsl ...

  4. [Spark内核] 第34课:Stage划分和Task最佳位置算法源码彻底解密

    本課主題 Job Stage 划分算法解密 Task 最佳位置算法實現解密 引言 作业调度的划分算法以及 Task 的最佳位置的算法,因为 Stage 的划分是DAGScheduler 工作的核心,这 ...

  5. Springboot学习04-默认错误页面加载机制源码分析

    Springboot学习04-默认错误页面加载机制源码分析 前沿 希望通过本文的学习,对错误页面的加载机制有这更神的理解 正文 1-Springboot错误页面展示 2-Springboot默认错误处 ...

  6. springboot+mybatis+bootstrap开发员工oa后台管理系统项目源码

    java项目源码详情描述:S020<springboot+mybatis+bootstrap开发员工oa后台管理系统项目源码>jboa项目有请假以及报销单的申请和审核session共享加登 ...

  7. 分布式服务弹性框架“Hystrix”实践与源码研究(一)

    文章初衷 为了应对将来在线(特别是无线端)业务量的成倍增长,后端服务的分布式化程度需要不断提高,对于服务的延迟和容错管理将面临更大挑战,公司框架和开源团队选择内部推广Netflix的Hystrix,一 ...

  8. Springboot与Thymeleaf模板引擎整合基础教程(附源码)

    前言 由于在开发My Blog项目时使用了大量的技术整合,针对于部分框架的使用和整合的流程没有做详细的介绍和记录,导致有些朋友用起来有些吃力,因此打算在接下来的时间里做一些基础整合的介绍,当然,可能也 ...

  9. Springboot系列:Springboot与Thymeleaf模板引擎整合基础教程(附源码)

    前言 由于在开发My Blog项目时使用了大量的技术整合,针对于部分框架的使用和整合的流程没有做详细的介绍和记录,导致有些朋友用起来有些吃力,因此打算在接下来的时间里做一些基础整合的介绍,当然,可能也 ...

随机推荐

  1. k8s service不能访问排错

    简介 对于新安装的 Kubernetes,经常出现的一个问题是 Service 没有正常工作.如果您已经运行了 Deployment 并创建了一个 Service,但是当您尝试访问它时没有得到响应,希 ...

  2. NOIP模拟测试21「折纸&#183;不等式」

    折纸 题解 考试时无限接近正解,然而最终也只是接近而已了 考虑模拟会爆炸,拿手折纸条试一试,很简单 考你动手能力 代码 #include<bits/stdc++.h> using name ...

  3. 初学springboot

    现在总是与数据库和前端打交道,让我觉得好厌烦,还是喜欢敲代码.最近问了几个朋友,都说潮流要学springCloud,然后学springCloud又要先学springboot,所以这段时间我会慢慢把sp ...

  4. 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  5. kubernetes的存活探针和就绪探针

    1.存活探针 使用Kubernetes的一个主要好处是,可以给Kubernetes-个容器列表来由其保持容器在集群中的运行.可以通过让Kubernetes创建pod资源,为其选择一个工作节点并在该节点 ...

  6. Vue(13)子组件与父组件超详细解析

    父组件和子组件 我们经常分不清什么是父组件,什么是子组件.现在来简单总结下:我们将某段代码封装成一个组件,而这个组件又在另一个组件中引入,而引入该封装的组件的文件叫做父组件,被引入的组件叫做子组件.具 ...

  7. JS中 var,let与const的区别

    1.在ES6(ES2015)出现之前,JavaScript中声明变量就只有通过 var 关键字,函数声明是通过 function 关键字,而在ES6之后,声明的方式有 var . let . cons ...

  8. Linux sudo命令——sudoers文件的配置

    Linux sudo命令与其配置文件/etc/sudoers   对linux有一定了解的人多少也会知道点关于sudo命令.sudo命令核心思想是权限的赋予 ,即某个命令的所属用户不是你自己,而你却有 ...

  9. ArchLinux安装步骤(一)

    本文为安装archlinux的教程,需要有硬盘分区,挂载等基础linux命令的了解还有vim的基本操作,不知道也没关系,这里有大神的视频教程ArchLinux指南. 确实是不是uefi模式 ls /s ...

  10. C语言:变量定义

    变量定义:用于为变量分配存储空间,还可为变量指定初始值.程序中,变量有且仅有一个定义.变量声明:用于向程序表明变量的类型和名字.定义也是声明,extern声明不是定义 定义也是声明:当定义变量时我们声 ...