情景分析

有时候在前端展示时,需要将电话号码,身份证等敏感信息过滤掉,显示成 *** 的字样;如果只是前端进行修改,那么其实数据还是会返回,只能进行后端的修改,

疑难点:

1:并不是所有页面都要进行模糊,比如管理员等操作不能进行模糊掉,

2:有部分导入的功能,导出的数据也可能需要模糊掉;新增时不能进行加密,修改有加密处理的,保存时需要再进行恢复;

3:一些返回的字段名字并不统一,有的叫 phoneNum,有的叫 phone,有点叫 managerPhone

4:部分前端组件比如客户下拉框等也包含身份证信息,同样需要进行脱敏处理;

5:返回结果进行处理时,可能是封装起来的对象,需要遍历加递归进行处理;

实现思路:

1:权限控制

设置页面给操作人员添加权限控制,哪些字段可以显示,哪些字段需要进行脱敏;

2:自定义注解,将需要进行模糊类型统一封装成一个实体,让需要脱敏的返回类型继承该实体,这样可以避免每一个实体中都去添加注解,然后进行AOP编程,将数据进行模糊处理;

切面的处理几乎每个都要搞,将其需要进行处理的权限字段放入到Redis中,修改权限控制时删除并更新Redis;

系统中数据显示进行脱敏处理-LMLPHP

@IgnoreEncrypt 注解,使用该注解标注的controller不会校验权限;

接口传入 ignoreEncrypt=1 或者使用 IgnoreEncrypt标注controller,都可以使该次请求不校验权限

@FieldRight注解:

@FieldRight(fieldRightType = FieldRightType.CARD_NO)
private String newcardNo;//证件号码,打*

实现代码:

1:声明脱敏的字段注解

/**
     * 标记字段 使用何种策略来脱敏
     */
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldRight {
    FieldRightType fieldRightType() default FieldRightType.OTHER;
}

其中脱敏类型可根据情况进行自定义

package com.xqc.commoncommon.enums;

public enum FieldRightType {

    /**
     * 证件号码
     */
    CARD_NO("cardNo","证件号码"),
    /**
     * 邮箱
     */
    EMAIL("email","邮箱"),
    /**
     * 联系电话
     */
    PHONE("phone","联系电话"),
    /**
     * 客户生日
     */
//    BIRTHDAY("birthday","客户生日"),
    /**
     * 联系地址
     */
    ADDRESS("address","联系地址"),
    /**
     * 证件地址
     */
    CARD_ADDRESS("cardAddress","证件地址"),
    /**
     * 注册地址
     */
    REGISTER_ADDRESS("registerAddress","注册地址"),
    /**
     * 工作地址
     */
    WORK_ADDRESS("workAddress","工作地址"),
    /**
     * 门户登录账号
     */
    LOGIN_NAME("loginName","门户登录账号"),
    /**
     * 其他(不受权限控制)
     */
    OTHER("other","其他"),

    private final String field;
    private final String fieldName;

    FieldRightType(String field, String fieldName) {
        this.field = field;
        this.fieldName = fieldName;
    }

    public String getField() {
        return field;
    }

    public String getFieldName() {
        return fieldName;
    }
}

2:自定义脱敏的统一实体

3:将需要脱敏的实体进行改造

4:对请求的统一处理

		//先从redis中查询权限,如果查询不到则从数据库中查询,并放在redis中
		//userId
		LoginUserInfo loginUserInfo = ServletUtil.getLoginUserInfo(request);
		String ignoreEncrypt = request.getParameter("ignoreEncrypt");
		if (loginUserInfo != null && !"1".equals(ignoreEncrypt)) {
			// 获取容器
			ServletContext sc = request.getSession().getServletContext();
			XmlWebApplicationContext cxt = (XmlWebApplicationContext) WebApplicationContextUtils.getWebApplicationContext(sc);
			// 从容器中获取DispersedCacheSerciceImpl
			if (cxt != null && cxt.getBean("DispersedCacheSerciceImpl") != null && iDispersedCacheSercice == null) {
				iDispersedCacheSercice = (DispersedCacheSerciceImpl) cxt.getBean("DispersedCacheSerciceImpl");
			}
			String key = "FieldRight:" + loginUserInfo.getUserId();
			Object o = iDispersedCacheSercice.get(key);
			List<String> userCustomerFieldRightList = new ArrayList<>();
			if (o != null) {
				userCustomerFieldRightList = JSONArray.parseArray(o.toString(), String.class);
			} else {
				UserCustomerFieldRightQuery query = new UserCustomerFieldRightQuery();
				query.setCompanyId(loginUserInfo.getCompanyId());
				query.setUserId(loginUserInfo.getUserId());
				//查询需要隐藏的字段
				query.setFieldRight(1);
				List<CommonUserCustomerFieldRightDTO> userCustomerFieldRight = iUserService.getUserCustomerFieldRight(query);
				//整理
				userCustomerFieldRightList = userCustomerFieldRight.stream().map(CommonUserCustomerFieldRightDTO::getField)
						.collect(Collectors.toList());
				iDispersedCacheSercice.add(key, userCustomerFieldRightList);
			}
			//查询该用户的客户权限,如果查询不到则表示全部放开
			request.setAttribute("userCustomerFieldRightList",userCustomerFieldRightList);
		}

5:AOP对返回结果统一处理

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.xqc.annotation.FieldRight;
import com.xqc.annotation.IgnoreEncrypt;
import com.xqc.enums.FieldRightType;
import com.xqc.utils.CommonUtil;

import java.lang.reflect.Field;
import java.util.List;

/**
 * controller的切面控制,目前用于字段权限控制<br/>
 * 关于字段权限控制,详情请查看{@link FieldRight}
 */
@Aspect
@Component
@Slf4j
public class CommonControllerAspect {
    /**
     * 切入所有添加{@link IgnoreEncrypt}注解的controller
     */
    @Pointcut("@annotation(com.xqc.annotation.IgnoreEncrypt)")
    public void pointcut(){}

    /**
     * 添加{@link IgnoreEncrypt}注解的controller在进入之前去除字段权限校验标志
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        requestAttributes.getRequest().setAttribute("userCustomerFieldRightList",null);
    }

    /**
     * 1、切入所有的controller
     * 2、目前(2021年8月4日)用于字段权限校验,需考虑该字段权限校验是否只过滤部分package下的controller
     */
    @Pointcut("execution(* com.xqc.*.controller.*.*(..))")
    public void allControllerPointCut(){}


    /**
     * 1、切入controller的返回后,
     * @param joinPoint
     * @param returnValue
     */
    @AfterReturning(value = "allControllerPointCut()", returning="returnValue")
    @SuppressWarnings("unchecked")
    public void afterController(JoinPoint joinPoint,Object returnValue) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        List<String> fieldRightList = (List<String>)requestAttributes.getRequest().getAttribute("userCustomerFieldRightList");
        try {
            dealFieldRight(returnValue,fieldRightList);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }


    /**
     * 字段权限处理主要逻辑
     * 1、 如果传入的是list,则遍历后递归
     * 2、 如果传入的是dto,则直接分析
     * 3、 传入的是其他类型,直接忽略
     */
    @SuppressWarnings("unchecked")
    public static boolean dealFieldRight(Object model, List<String> fieldRightList) throws Exception{
        //如果需要校验的字段,则直接返回false
        if (fieldRightList == null || fieldRightList.isEmpty()){
            return false;
        }
        //如果需要处理的object为空,则直接返回
        if (model == null){
            return false;
        }
        //根据typeName来判断是dto还是list
        String typeName = model.getClass().getTypeName();
        //判断是否是list,list的typeName:  java.util.***List,所以根据java.utll和小写的list为关键字进行判断,若同时出现则认定为是list
        if (typeName.contains("java.util") && typeName.toLowerCase().contains("list")){
            /*
                 是list
                 循环对每一个元素递归处理
             */
            List modelList = (List)model;
            if (modelList.isEmpty()){
                return false;
            }
            for (Object item : modelList) {
                if (item == null){
                    continue;
                }
                //递归进行处理,但是当第一次遇到的元素无需进行处理的时候,表示后续的item也无需处理了
                boolean canDoNext = dealFieldRight(item,fieldRightList);
                if (!canDoNext){
                    break;
                }
            }
            return true;
        }else if (typeName.contains("com.xqc")){
            //不是list,那么就根据com.xqc判断是不是项目的Object
            //需要循环读取父类,直到遇到Object为止:Object的superClass是空
            Class checkClass = model.getClass();
            while (checkClass.getSuperclass()!=null){
                Field[] fields = checkClass.getDeclaredFields();
                for (Field field : fields) {
                    //如果是一个list,那么需要递归进行处理
                    String type = field.getType().toString();
                    if (type.contains("java.util") && type.toLowerCase().contains("list")){
                        field.setAccessible(true);
                        List o = (List)field.get(model);
                        if (o == null){
                            continue;
                        }
                        if (o.isEmpty()){
                            continue;
                        }
                        for (Object item : o) {
                            if (item == null){
                                continue;
                            }
                            boolean canDoNext = dealFieldRight(item,fieldRightList);
                            if (!canDoNext){
                                break;
                            }
                        }
                    }
                    //如果field是dto,也要递归处理
                    if (type.contains("com.xqc")){
                        field.setAccessible(true);
                        Object o = field.get(model);
                        if (o != null){
                            dealFieldRight(model,fieldRightList);
                        }
                    }
                    String fieldName;
                    FieldRight annotation = field.getAnnotation(FieldRight.class);
                    if (annotation != null){
                        if (annotation.fieldRightType() == FieldRightType.OTHER){
                            continue;
                        }else{
                            fieldName = annotation.fieldRightType().getField();
                        }
                    }else {
                        //没有注解的不进行加密处理
                        continue;
                    }
                    if (fieldRightList.contains(fieldName)) {
                        field.setAccessible(true);
                        Object o1 = field.get(model);
                        if (o1 == null || "".equals(o1.toString())){
                            continue;
                        }
                        //如果是String就设置为星号,否则设置为空
                        if ("class java.lang.String".equals(field.getGenericType().toString())){
                            //判断是否是cardNo
                            if (fieldName.equals(FieldRightType.CARD_NO.getField())){
                                //获取cardNo
                                field.set(model, CommonUtil.cardNoSet(o1.toString()));
                            }else{
                            field.set(model, "******");

                            }
                        }else{
                            field.set(model,null);
                        }
                    }
                }
                checkClass = checkClass.getSuperclass();
            }
            return true;
        }else {
            //如果一个其他类型传进来进行数据处理,直接忽略即可,只需要处理list和com.xqc下的dto
            return false;
        }
    }
}
11-02 04:25