Springboot 配置文件、隐私数据脱敏的最佳实践(二)
紧接着测试一下切面注解的效果,我们对字段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-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生成器。
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);
}
解密
我是小富~,如果对你有用在看、关注支持下,咱们下期见~
文章转自公众号:程序员小富