大家好!我是小富~
这几天公司在排查外部数据账号透露,起因是发现某些实习生小可爱竟然连带着账号、明码将源码私传到 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-nd…
@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;
}。。。}
紧接着测试一下切面注解的成果,咱们对字段 mobile
、address
加上注解 @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-nd…
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
生成器。
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);
}
我是小富,下期见~
整顿了几百本各类技术电子书,有须要的同学自取。技术群快满了,想进的同学能够加我好友,和大佬们一起吹吹技术。
电子书地址
集体公众号:程序员内点事,欢送交换