前言
在《
Springboot项目如何设计接口中敏感字段的加密、解密
》中,分享了Springboot项目里写入请求、读取请求的接口中敏感字段数据如何加密、解密,其实对于敏感数据的处理方式,除了在服务端进行数据加密、解密,在前端数据展示的时候,也要进行相应的“加密”处理,以防止用户的隐私信息被泄漏,这里的“加密”实际是数据脱敏。由于数据脱敏和数据加密的处理过程是完全不同的,所以我认为这是两个概念;当然,仁者见仁,智者见者,这是我的个人理解。单独起一篇文章,就来好好好盘一盘数据脱敏和数据加密的区别以及实现方式。
数据脱敏与数据加密
数据脱敏
数据脱敏是指通过一定规则把敏感数据的实际值转换成虚构的值,以便更好的保证数据的隐私性, 同时又保证数据的可用性,整个过程是不可逆的,如脱敏后的手机号码(155********);
数据加密
数据加密是指对重要数据进行重新编码处理,得到一个虚构的值,以便更好的保护数据的安全性,获取实际数据的唯一办法就是使用密钥再进行解密,可以有效防止数据泄漏时直接暴露实际数据;
小结:数据脱敏与数据加密的根本目的基本相同,即为了保护数据的安全性和隐私性,防止实际数据的泄漏;不同的则是具体的过程和方法,数据加密的方法是通过加密算法,用一个虚构的值代替实际的数据,在数据读取的时候则通过密钥解密,把虚构的值转换为实际的数据,整个过程是可逆的,而数据脱敏则是单向不可逆的,即在数据展示的时候到,根据一定规则,把实际数据转换成虚构的值。
需求描述
数据脱敏要达到效果就是在最终最外输出的时候,可以按照一定规则把敏感数据的实际值替换成虚构的值,下面示例来具体说明一下,手机号码、身份证号码、门牌号码等敏感数据的脱敏处理,具体的脱敏规则是:
1、手机号码最后8位以“*”代替;
2、身份证号码中间出生年月日部分以“*”代替;
3、门牌号码前三位以“*”代替;
实现原理
使用AOP(面向切面编程),自定义解密切面注解,标记在目标方法上作为切点,在切点前后织入环绕通知;在环绕通知方法内目标方法执行完后,判断输出结果对象的字段属性上是否标记了解密字段注解,如果判断结果为是,则需要解密、脱敏处理;
实现方案
环境配置
jdk版本:1.8开发工具:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE
mybatis-spring-boot-starter:2.1.4
依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
示例代码
敏感数据通常是加密后存储在数据库的表中,对外输出敏感数据的时候,需要从数据库表中读取出数据进行解密处理,还原成实际数据后对外输出;因此需要对敏感数据在页面上脱敏展示,最适合的时机就是数据解密后,紧接着就进行数据脱敏处理,具体的方法如下:
1、对《
Springboot项目如何设计接口中敏感字段的加密、解密
》中的解密字段注解(@DecryptField)进行改造,增加三个属性,分别是是否开启敏感数据脱敏的开关、脱敏开始位置索引、脱敏从开始位置向后偏移量(如果有特殊需要,还可以定义一下用于代替敏感数据的字符,一般情况下,都使用的是“*”);
@Target(value = {ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
//是否开启敏感数据脱敏处理,默认不开启
boolean open() default false;
//脱敏开始位置索引
int start() default 0;
//脱敏从开始位置向后偏移量
int offset() default 6;
}
2、在需要脱敏的字段上,标记@DecryptField注解,开启数据脱敏,并注明脱敏开始的位置索引以及向后的偏移量;
@Slf4j
@Data
public class Person {
private Integer id;
private String userName;
private String loginNo;
@EncryptField
@DecryptField(open = true,start = 3,offset = 8)
private String phoneNumber;
private String sex;
@DecryptField(open = true,start = 6,offset = 8)
@EncryptField
private String IDCard;
private String address;
@EncryptField
@DecryptField(open = true,start = 0,offset = 3)
private String houseNumber;
}
3、对自定义解密切面类(DecryptAop)进行改造,原来的逻辑不变:用@NeedDecrypt注解定义解密切点,在解密切点的环绕通知方法里执行完具体的业务处理方法之后,判断输出对象的参数字段是否标记了@DecryptField(解密字段注解),如果判断结果为true,则使用java反射对该 字段进行解密处理;增加逻辑:在解密处理完成后,判断是否开启了敏感数据脱敏处理,如果判断结果为真,则根据脱敏规则进行脱敏处理;
@Component
@Aspect
@Slf4j
public class DecryptAop {
/**
* 定义需要解密的切入点
*/
@Pointcut(value = "@annotation(com.fanfu.anno.NeedDecrypt)")
public void pointcut() {
}
/**
* 命中的切入点时的环绕通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("//环绕通知 start");
//执行目标方法
Object result = proceedingJoinPoint.proceed();
//判断目标方法的返回值类型
if (result instanceof List) {
for (Object tmp : ((List) result)) {
//数据脱敏处理逻辑
this.deepProcess(tmp);
}
} else {
this.deepProcess(result);
}
log.info("//环绕通知 end");
return result;
}
public void deepProcess(Object obj) throws IllegalAccessException {
if (obj != null) {
//取出输出对象的所有字段属性,并遍历
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//判断字段属性上是否标记DecryptField注解
if (declaredField.isAnnotationPresent(DecryptField.class)) {
//如果判断结果为真,则取出字段属性数据进行解密处理
declaredField.setAccessible(true);
Object valObj = declaredField.get(obj);
if (valObj != null) {
String value = valObj.toString();
//加密数据的解密处理
value = this.decrypt(value);
DecryptField annotation = declaredField.getAnnotation(DecryptField.class);
boolean open = annotation.open();
//数据解密后,判断是否开启了数据脱敏处理;
if (open) {
//如果开启,则开始进行数据脱敏处理
int start = annotation.start();
int offset = annotation.offset();
value = this.secret(value, start, offset);
}
//把解密、脱敏后的数据重新赋值
declaredField.set(obj, value);
}
}
}
}
}
private String decrypt(String value) {
//这里特别注意一下,对称加密是根据密钥进行加密和解密的,加密和解密的密钥是相同的,一旦泄漏,就无秘密可言,
//“fanfu-csdn”就是我自定义的密钥,这里仅作演示使用,实际业务中,这个密钥要以安全的方式存储;
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), "fanfu-csdn".getBytes()).getEncoded();
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
String decryptStr = aes.decryptStr(value);
return decryptStr;
}
private String secret(String value, Integer start, Integer limit) {
//如果有特殊需要,还可以定义其他用于代替敏感数据的字符,一般情况下,使用的是“*”
char[] chars = value.toCharArray();
for (int i = start; i < start + limit; i++) {
chars[i] = '*';
}
return String.valueOf(chars);
}
}
脱敏结果
总结
在《
Springboot项目如何设计接口中敏感字段的加密、解密
》和《
Springboot项目如何设计接口中敏感数据的脱敏展示?
》中,总的来说是通过使用AOP(面向切面编程),在以目标方法为切点,在切点前后织入环绕通知,实现敏感字段数据的加密、解密和脱敏展示。那么有一个问题不知道大家想过没?敏感字段数据是加密存储在数据库的表中,对外输出的敏感字段数据也是脱敏后的结果,如果需要对这些敏感字段进行模模糊查询,还用原来的通过sql的where从句的like来模糊查询的方式肯定是不行的,那么应该怎么实现呢?这是一件麻烦事。