一、为什么要数据脱敏?
数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
通过数据脱敏产品,可以有效防止企业内部对隐私数据的滥用,防止隐私数据在未经脱敏的情况下从企业流出。满足企业既要保护隐私数据,同时又保持监管合规,满足企业合规性。
二、数据脱敏方案
2.1 静态数据脱敏(SDM)
静态数据脱敏(SDM)一般是通过变形、替换、屏蔽、保格式加密(FPE)等算法,将生产数据导出至目标的存储介质;脱敏后的数据,实际已经改变了源数据的内容,使用数据时需要对数据进行逆向还原。
2.2 动态数据脱敏(DDM)
动态数据脱敏(DDM)是获取数据时,对各种不同的需求,通过技术手段使输出的数据去除敏感信息,完成脱敏。例如:访问IP、MAC、数据库用户、客户端工具、操作系统用户、主机名、时间、影响行数等,在匹配成功后通过改写查询SQL或者拦截防护返回脱敏后的数据到应用端,从而实现敏感数据的脱敏。动态数据脱敏实际上未对源数据的内容做任何改变。
三、实现思路
3.1 使用实体类上字段注解完成脱敏
创建一个自定义注解,在实体类中标识需要脱敏的字段,然后使用拦截器将返回的字段进行脱敏。
3.2 使用方法注解完成脱敏
创建两个自定义注解,将注解放在返回值的方法上,并使用注解标记需要脱敏的字段,使用AOP完成脱敏。
四、 实现步骤(本文实现3.2的方法)
4.1 pom引入工具包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${alibaba.fastjson.version}</version>
</dependency>
4.2 创建自定义注解
import java.lang.annotation.*;
/**
* @author luoqifeng
* @date 2023/05/19 14:25
* @apiNote 用于标记当前方法需要脱敏
* 使用这个注解进行脱敏 建议在返回值时使用 Result 包装一下,可以确保每一个传入的值都能转换成 JSONObject 类型
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveWord {
Sensitive[] value();
}
创建标记需要脱敏的字段的自定义注解
import cn.hutool.core.util.DesensitizedUtil;
import static cn.hutool.core.util.DesensitizedUtil.DesensitizedType.FIXED_PHONE;
/**
* @author luoqifeng
* @date 2023/05/19 14:25
*
*/
public @interface Sensitive {
/**
* json path 的标识
* @return
*/
String jsonPath();
/**
* 脱敏的字段的数据分类, 默认是座机号码类型脱敏
* @return
*/
DesensitizedUtil.DesensitizedType desensitizedType() default FIXED_PHONE;
}
4.3 创建数据脱敏切面
直接复制代码会因为没有引用上面的两个自定义类报错,先复制上面两个自定义注解,然后在这里面引入就好
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import static cn.hutool.core.util.DesensitizedUtil.DesensitizedType.FIXED_PHONE;
/**
* 数据脱敏切面
*/
@Slf4j
@Aspect
@Component
public class SensitiveAspect {
@Around("@annotation(sensitiveWord)")
public Object around(ProceedingJoinPoint pjp, SensitiveWord sensitiveWord) throws Throwable{
Object object = pjp.proceed();
try {
Class<?> aClass = object.getClass();
// 改变返回值
Sensitive[] value = sensitiveWord.value();
JSONObject ob = JSON.parseObject(JSON.toJSONString(object));
// 循环处理每一个 json 路径下的值
for (Sensitive s: value) {
replace(ob, s);
}
object = JSON.parseObject(JSON.toJSONString(ob), aClass);
}catch (Exception e ){
log.info("数据脱敏失败:"+e.getMessage());
}
return object;
}
/**
* 脱敏
* */
private Object sensitiveByString(Object value) {
if (StringUtils.isNotEmpty(value.toString())) {
String st = Convert.toStr(value);
st = st.substring(0, st.length() - 3 > 0 ? 3 : st.length()) + "****" + st.substring(Math.max(st.length() - 4, 0));
return st;
}
return value;
}
/**
* 敏感数据替换
*
* @param jsonObject
* @param s
*/
private void replace(JSONObject jsonObject, Sensitive s) {
// 只有传入的 JSON 路径在 这个 JSONObject 中才会进行脱敏处理
if (JSONPath.contains(jsonObject, s.jsonPath())) {
// 查询是否有数组 列表 类型的数据需要脱敏
int index = s.jsonPath().lastIndexOf("[*]");
if (index > -1) {
String prefix = StrUtil.subPre(s.jsonPath(), index);
String suffix = StrUtil.subSuf(s.jsonPath(), index + 3);
// 提取json 路径下的 数组\链表 元素
Object eval = JSONPath.eval(jsonObject, prefix);
// 将数组\链表 元素 转为 JSONArray 方便做 统一格式处理
JSONArray jsonArray = (JSONArray) eval;
int size = jsonArray.size();
for (int i = 0; i < size; i++) {
// 由于脱敏数组内部的参数传入格式为 :jsonPath = "$.datas.records[*].username"
// 所以需要重新组装 jsonPath 将 * 号 替换成具体的值
String indexJsonPath = StrUtil.strBuilder().append(prefix).append("[").append(i).append("]").append(suffix).toString();
// 使用 cn.hutool.core.convert Convert.toStr 转换为字符串 如果给定的值为null,或者转换失败,返回默认值null,这样可以减少报错,避免程序异常
String desensitized = Convert.toStr(JSONPath.eval(jsonObject, indexJsonPath));
if (StrUtil.isBlank(desensitized)) {
continue;
}
// 如果是默认指定 则使用默认方式脱敏
if (s.desensitizedType() == FIXED_PHONE) {
desensitized = sensitiveByString(desensitized).toString();
}else {
// 否则使用 cn.hutool.core.util 进行数据脱敏
desensitized = DesensitizedUtil.desensitized(desensitized, s.desensitizedType());
}
// 使用JSON 路径操作,将已经脱敏的新数据,放入之前未脱敏的数据地址处,替换未脱敏数据
JSONPath.set(jsonObject, indexJsonPath, desensitized);
}
} else {
// 使用 cn.hutool.core.convert Convert.toStr 转换为字符串 如果给定的值为null,或者转换失败,返回默认值null,这样可以减少报错,避免程序异常
Object eval = JSONPath.eval(jsonObject, Convert.toStr(s.jsonPath()));
String desensitized = "";
if (s.desensitizedType() == FIXED_PHONE) {
desensitized = sensitiveByString(Convert.toStr(eval)).toString();
}else {
desensitized = DesensitizedUtil.desensitized(Convert.toStr(eval), s.desensitizedType());
}
JSONPath.set(jsonObject, s.jsonPath(), desensitized);
}
}
}
}
4.4 测试使用
4.4.1 在测试的controller 中使用如下代码
@PostMapping("/getUserInfo")
@SensitiveWord({
@Sensitive(jsonPath = "$.datas.name",desensitizedType = CHINESE_NAME),
@Sensitive(jsonPath = "$.datas.phone",desensitizedType = MOBILE_PHONE),
@Sensitive(jsonPath = "$.datas.bankCard",desensitizedType = BANK_CARD),
})
public Result<Object> getUserInfo() {
Map<String, String> map = new HashMap<>();
map.put("name","张三");
map.put("phone","18111111111");
map.put("bankCard","6227112222211111211");
return Result.succeed(map);
}
响应结果为:
4.4.2 测试实体类数组脱敏
@GetMapping("getInfo2")
@SensitiveWord({
@Sensitive(jsonPath = "$.datas.username",desensitizedType = MOBILE_PHONE),
@Sensitive(jsonPath = "$.datas.type"),
})
public Result<Object> getCaseInfo2() {
User user = new User()
.setUsername("18111111111")
.setType("ceshi");
return Result.succeed(user);
}
输出结果
4.4.3 测试 数组/list 类型脱敏
@GetMapping("getInfo3")
@SensitiveWord(@Sensitive(jsonPath = "$.datas[*].username"))
public Result<List<User>> getCaseInfo(){
List<User> users = new ArrayList<>();
users.add( new User()
.setUsername("18111111111")
.setType("ceshi")
);
users.add( new User()
.setUsername("18122222222")
.setType("ceshi2")
);
return Result.succeed(users);
}
输出结果:
测试完成,数据脱敏成功
五、 说在最后
动态数据脱敏的方式很多,不局限于自定义注解,也不局限于AOP方式,可行方案,可以是在数据SQL时进行脱敏,可以在业务逻辑里面进行脱敏,可以在网关脱敏等等,根据自身业务规则实现即可。