Spring数据脱敏

方法介绍

我利用了AOP和标签的原理来进行组合,实现脱敏的效果,不足之处就是在于如果查询条数多了会有轻微卡顿,使用分页时完全看不出来卡顿。

接下来我们来实现

  1. 第一步创建配合AOP使用的注解
    注解的含义
    这里有四种类型的元注解:

1@Documented —— 指明拥有这个注解的元素可以被javadoc此类的工具文档化。这种类型应该用于注解那些影响客户使用带注释的元素声明的类型。如果一种声明使用Documented进行注解,这种类型的注解被作为被标注的程序成员的公共API。

2@Target——指明该类型的注解可以注解的程序元素的范围。该元注解的取值可以为TYPE,METHOD,CONSTRUCTOR,FIELD等。如果Target元注解没有出现,那么定义的注解可以应用于程序的任何元素。

3@Inherited——指明该注解类型被自动继承。如果用户在当前类中查询这个元注解类型并且当前类的声明中不包含这个元注解类型,那么也将自动查询当前类的父类是否存在Inherited元注解,这个动作将被重复执行知道这个标注类型被找到,或者是查询到顶层的父类。

4@Retention——指明了该Annotation被保留的时间长短。RetentionPolicy取值为SOURCE,CLASS,RUNTIME。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
  * 属性标签,配合AOP使用,单独使用无效
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface ReadableSensitiveVerify {

    // 枚举的值
    ReadableSensitiveTypeEnum value();


}

建好标签之后我们需要为需要脱敏的字段创建相应的枚举

枚举是一种特殊的类,特殊在它的对象是有限的几个常量对象。它既是一种类(class)类型却又比类类型多了些特殊的约束,枚举的本质是一种受限制的类。

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ReadableSensitiveTypeEnum {

    /**
     * 身份证编号
     */
    ID_CARD,

    /**
     * 地址/住址
     */
    ADDRESS,

    /**
     * 姓名
     */
    NAME,

    /**
     * 手机号
     */
    PHONE,

    /**
     * 手机号
     */
    EMAIL,

    /**
     * 银行卡号
     */
    BANK_CARD_NO;

}

枚举创建完成之后我们就可以在实体属性上添加标签了,等AOP创建完成后有标签的属性就可以实现脱敏的效果了。

    /**
     * 证件号码
     */
    @TableField("opIDCode")
    @ReadableSensitiveVerify(ReadableSensitiveTypeEnum.ID_CARD)
    private String opIDCode;
  1. 第二步创建AOP来实现数据脱敏
  • 首先我们要了解AOP是什么东西。
    AOP是Aspect Oriented Programming的缩写,意思是面向方面编程,与OOP(Object Oriented Programming)面向对象编程对等,都是一种编程思想。从OOP角度分析,我们关注业务的处理逻辑,是属于纵向的行为,从AOP角度分析,我们关注对象行为发生时的问题,是属于横向的行为。AOP有以下概念术语:
    Aspect(切面):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式)或者基于@Aspect注解的方式来实现。
    Joinpoint(连接点):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
    Advice(通知):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
    Pointcut(切入点):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
    Introduction(引入):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
    Target Object(目标对象): 被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
    Weaving(织入):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
  • 然后让我们创建我们的AOP
package com.lyxcn.witcity.service.village.Aspect;

import com.lyxcn.smartcity.yq.commons.config.security.SecurityConfig.LoginConfig.SecurityUtils;
import com.lyxcn.witcity.service.village.utils.desensitization.DesensitizationUtils;
import com.lyxcn.witcity.service.village.utils.desensitization.ReadableSensitiveTypeEnum;
import com.lyxcn.witcity.service.village.utils.desensitization.ReadableSensitiveVerify;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * DAO切面,插入创建人,创建时间,修改人,修改时间
 * @Auther LIUXIAO
 * @Date 2019/10/14
 */
@Aspect
@Component
@Configuration
public class DaoAspectCommon {

    /**
     * @Author lx
     * @Description findPage分页脱敏
     * @Date 2022/6/22 17:45
     * @Param []
     * @return void
     */
    @Pointcut("execution(* com.lyxcn.witcity.service.village.mapper.*.*.findPage(..))")
    public void findPagePointCut() {
    }

    /**
     * @Author lx
     * @Description info详情脱敏
     * @Date 2022/6/22 17:45
     * @Param []
     * @return void
     */
    @Pointcut("execution(* com.lyxcn.witcity.service.village.mapper.*.*.info(..))")
    public void infoPointCut() {
    }

    /**
     * @Author lx
     * @Description findPage分页脱敏
     * @Date 2022/6/22 17:45
     * @Param []
     * @return void
     */
    @AfterReturning(value = "findPagePointCut()",returning = "methodResult")
    public Object findPagePointCut(JoinPoint proceedingJoinPoint, Object methodResult)  throws Throwable  {
    	// 获取当前访问权限的权限码
        List<String> module = null;
        try {
            module = SecurityUtils.getLoginInfo().getModule();
        } catch (Exception e) {
            return methodResult;
        }
        // 在权限码中找到脱敏的权限码并填充到list中
        List result =module.stream().filter(x -> x.contains("ROLE_display")).collect(Collectors.toList());
        int phone = 0,IDCode = 0, skip = 0, noSkip = 0;
        // 判定电话
        if (result.contains("ROLE_display.IDCode")){
            if (result.contains("ROLE_displayHalf.IDCode")){
                phone = 2;
            }
            phone = 1;
        }
        if (result.contains("ROLE_displayHalf.IDCode")){
            phone = 2;
        }
        // 判定身份证
        if (result.contains("ROLE_display.Phone")){
            if (result.contains("ROLE_displayHalf.Phone")){
                IDCode = 2;
            }
            IDCode = 1;
        }
        if (result.contains("ROLE_displayHalf.Phone")){
            IDCode = 2;
        }
        //获取执行方法的参数
        if (Objects.isNull(methodResult)) {
            return null;
        }
        List list = null;
        try {
            list = (List)methodResult;
        } catch (Exception e) {
        }
        for (Object map:list){
            Field[] fields = map.getClass().getDeclaredFields();
            for (Field field :fields){
                field.setAccessible(true);
                // 方法参数中属性的指定注解
                ReadableSensitiveVerify annotation = field.getAnnotation(ReadableSensitiveVerify.class);
                noSkip++;
                try {
                    if (Objects.nonNull(annotation)) {
                        if (Objects.nonNull(field)){
                        ReadableSensitiveTypeEnum typeEnum = annotation.value();
                        String valueStr = field.getName();
                        // 利用BeanUtils.getProperty()取出参数
                        String property = BeanUtils.getProperty(map, valueStr);
                            switch (typeEnum) {
                            case PHONE:
                                if (phone == 2){
                                // 脱敏工具类脱敏
                                    property = DesensitizationUtils.desensitizedPhoneNumber(property);
                                }
                                if (phone == 0){
                                    property = null;
                                }
                                break;
                            case ID_CARD:
                                if (IDCode == 2){
                                    property = DesensitizationUtils.desensitizedIdNumber(property);
                                }
                                if (IDCode == 0){
                                    property = null;
                                }
                                break;
                            default:
                        }
                        // 利用BeanUtils.setProperty() 设置脱敏后的参数
                        BeanUtils.setProperty(map ,valueStr, property);
                    }
                    }else{
                        skip++;
                    }
                }catch (Exception e){

                }
            }
            if (noSkip == skip){
                return methodResult;
            }else{
                skip = 0;
                noSkip = 0;
            }
        }
        return methodResult;
    }


    /**
     * @Author lx
     * @Description info详情脱敏
     * @Date 2022/6/22 17:45
     * @Param []
     * @return void
     */
    @AfterReturning(value = "infoPointCut()",returning = "methodResult")
    public Object infoPointCut(JoinPoint proceedingJoinPoint, Object methodResult)  throws Throwable  {
        List<String> module = null;
        try {
            module = SecurityUtils.getLoginInfo().getModule();
        } catch (Exception e) {
            e.printStackTrace();
            return methodResult;
        }
        List result =module.stream().filter(x -> x.contains("ROLE_display")).collect(Collectors.toList());
        int phone = 0,IDCode = 0;
        // 判定电话
        if (result.contains("ROLE_display.IDCode")){
            if (result.contains("ROLE_displayHalf.IDCode")){
                phone = 2;
            }
            phone = 1;
        }
        if (result.contains("ROLE_displayHalf.IDCode")){
            phone = 2;
        }
        // 判定身份证
        if (result.contains("ROLE_display.Phone")){
            if (result.contains("ROLE_displayHalf.Phone")){
                IDCode = 2;
            }
            IDCode = 1;
        }
        if (result.contains("ROLE_displayHalf.Phone")){
            IDCode = 2;
        }
        //获取执行方法的参数
        Field[] fields = new Field[0];
        try {
            fields = methodResult.getClass().getDeclaredFields();
        } catch (Exception e) {
//            e.printStackTrace();
        }
        for (Field field :fields){
                field.setAccessible(true);
                //方法参数中属性的指定注解
                ReadableSensitiveVerify annotation = field.getAnnotation(ReadableSensitiveVerify.class);
                try {
                    if (Objects.nonNull(annotation)) {
                        if (Objects.nonNull(field)){
                        ReadableSensitiveTypeEnum typeEnum = annotation.value();
                        String valueStr = field.getName();
                        String property = BeanUtils.getProperty(methodResult, valueStr);
                            switch (typeEnum) {
                                case PHONE:
                                    if (phone == 2){
                                        property = DesensitizationUtils.desensitizedPhoneNumber(property);
                                    }
                                    if (phone == 0){
                                        property = null;
                                    }
                                    break;
                                case ID_CARD:
                                    if (IDCode == 2){
                                        property = DesensitizationUtils.desensitizedIdNumber(property);
                                    }
                                    if (IDCode == 0){
                                        property = null;
                                    }
                                    break;
                            default:
                        }
                        BeanUtils.setProperty(methodResult,valueStr, property);
                    }
                    }
                }catch (Exception e){

                }
            }

        return methodResult;
    }

}
  • 最后就是我们的脱敏工具类了
    在这之前我们先来了解一下正则是什么?

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

这里的脱敏就有用到了正则匹配,通过上面的标签来匹配相应字段进行相应的脱敏处理。

import com.google.common.base.Strings;
import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DesensitizationUtils {

    /**
     * @description: 名字脱敏
     * 脱敏规则: 隐藏中中间部分,比如:李某人 置换为 李*人 , 李某置换为 *某,司徒司翘置换为 司**翘
     * @return:
     * @author: lx
     * @time: 2022/6/22
     */
    public static String desensitizedName(String fullName){
        if (!Strings.isNullOrEmpty(fullName)) {
            int length = fullName.length();
            if(length == 2){
                return "*".concat(fullName.substring(1));
            }else if(length == 3){
                return StringUtils.left(fullName,1).concat("*").concat(StringUtils.right(fullName,1));
            }else if(length > 3){
                return StringUtils.left(fullName,1).concat(generateAsterisk(fullName.substring(1,length-1).length())).concat(StringUtils.right(fullName,1));
            }else {
                return fullName;
            }
        }
        return fullName;
    }


    /**
     * @description: 手机号脱敏,脱敏规则: 保留前三后四, 比如15566026528置换为155****6528
     * @return:
     * @author: lx
     * @time: 2022/6/22
     */
    public static String desensitizedPhoneNumber(String phoneNumber){
        if(StringUtils.isNotEmpty(phoneNumber)){
            int length = phoneNumber.length();
            if(length == 11){
                return phoneNumber.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2");
            }else if(length > 2){
                return StringUtils.left(phoneNumber,1).concat(generateAsterisk(phoneNumber.substring(1,length-2).length())).concat(StringUtils.right(phoneNumber,1));
            }else {
                return phoneNumber;
            }
        }
        return phoneNumber;
    }


    /**
     * @description: 身份证脱敏
     * 脱敏规则: 保留前六后三, 适用于15位和18位身份证号:
     * 原身份证号(15位):210122198401187,脱敏后的身份证号:210122******187
     * 原身份证号(18位):210122198401187672,脱敏后的身份证号:210122*********672
     * @return:
     * @author: lx
     * @time: 2022/6/22
     */
    public static String desensitizedIdNumber(String idNumber){
        if (!Strings.isNullOrEmpty(idNumber)) {
            int length = idNumber.length();
            if (length == 15){
                return idNumber.replaceAll("(\\w{6})\\w*(\\w{3})", "$1******$2");
            }else if (length == 18){
                return idNumber.replaceAll("(\\w{6})\\w*(\\w{3})", "$1*********$2");
            }else if(length > 9){
                return StringUtils.left(idNumber,6).concat(generateAsterisk(idNumber.substring(6,length-3).length())).concat(StringUtils.right(idNumber,3));
            }
        }
        return idNumber;
    }


    /**
     * @description: 电子邮箱脱敏,脱敏规则:电子邮箱隐藏@前面的3个字符
     * @return:
     * @author: lx
     * @time: 2022/6/22
     */
    public static String desensitizationEmail(String email) {
        if (StringUtils.isEmpty(email)) {
            return email;
        }
        String encrypt = email.replaceAll("(\\w+)\\w{3}@(\\w+)", "$1***@$2");
        if (email.equalsIgnoreCase(encrypt)) {
            encrypt = email.replaceAll("(\\w*)\\w{1}@(\\w+)", "$1*@$2");
        }
        return encrypt;
    }


    /**
     * @description: 地址脱敏,脱敏规则:从第4位开始隐藏,隐藏8位
     * @return:
     * @author: lx
     * @time: 2022/6/22
     */
    public static String desensitizedAddress(String address){
        if (!Strings.isNullOrEmpty(address)) {
            int length = address.length();
            if(length > 4 && length <= 12){
                return StringUtils.left(address, 3).concat(generateAsterisk(address.substring(3).length()));
            }else if(length > 12){
                return StringUtils.left(address,3).concat("********").concat(address.substring(11));
            }else {
                return address;
            }
        }
        return address;
    }


    /**
     * @description: 银行账号脱敏, 脱敏规则:银行账号保留前六后四
     * @return:
     * @author: lx
     * @time: 2022/6/23
     */
    public static String desensitizedAddressBankCardNum(String acctNo) {
        if (StringUtils.isNotEmpty(acctNo)) {
            String regex = "(\\w{6})(.*)(\\w{4})";
            Matcher m = Pattern.compile(regex).matcher(acctNo);
            if (m.find()) {
                String rep = m.group(2);
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < rep.length(); i++) {
                    sb.append("*");
                }
                acctNo = acctNo.replaceAll(rep, sb.toString());
            }
        }
        return acctNo;
    }


    /**
     * @description: 返回指定长度*字符串
     * @return:
     * @author: lx
     * @time: 2022/6/22
     */
    private static String generateAsterisk(int length){
        String result = "";
        for (int i = 0; i < length; i++) {
            result += "*";
        }
        return result;
    }

}

这是最终的效果图
Spring数据脱敏_第1张图片

虽然有些美中不足,但足以适合我当前的项目环境,以后我还会进行修改的,谢谢大家的阅读。

你可能感兴趣的:(java,spring,java,apache)