Springboot项目如何设计接口中敏感数据的脱敏展示?

  • Post author:
  • Post category:其他


前言

在《

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来模糊查询的方式肯定是不行的,那么应该怎么实现呢?这是一件麻烦事。



版权声明:本文为fox9916原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。