java 通过注解实现数据动态脱敏

一、为什么要数据脱敏?

数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
通过数据脱敏产品,可以有效防止企业内部对隐私数据的滥用,防止隐私数据在未经脱敏的情况下从企业流出。满足企业既要保护隐私数据,同时又保持监管合规,满足企业合规性

二、数据脱敏方案

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时进行脱敏,可以在业务逻辑里面进行脱敏,可以在网关脱敏等等,根据自身业务规则实现即可。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值