系統中資料顯示進行脫敏處理

阿蒼老師發表於2021-11-01

情景分析

有時候在前端展示時,需要將電話號碼,身份證等敏感資訊過濾掉,顯示成 *** 的字樣;如果只是前端進行修改,那麼其實資料還是會返回,只能進行後端的修改,

疑難點:

1:並不是所有頁面都要進行模糊,比如管理員等操作不能進行模糊掉,

2:有部分匯入的功能,匯出的資料也可能需要模糊掉;新增時不能進行加密,修改有加密處理的,儲存時需要再進行恢復;

3:一些返回的欄位名字並不統一,有的叫 phoneNum,有的叫 phone,有點叫 managerPhone

4:部分前端元件比如客戶下拉框等也包含身份證資訊,同樣需要進行脫敏處理;

5:返回結果進行處理時,可能是封裝起來的物件,需要遍歷加遞迴進行處理;

實現思路:

1:許可權控制

設定頁面給操作人員新增許可權控制,哪些欄位可以顯示,哪些欄位需要進行脫敏;

2:自定義註解,將需要進行模糊型別統一封裝成一個實體,讓需要脫敏的返回型別繼承該實體,這樣可以避免每一個實體中都去新增註解,然後進行AOP程式設計,將資料進行模糊處理;

切面的處理幾乎每個都要搞,將其需要進行處理的許可權欄位放入到Redis中,修改許可權控制時刪除並更新Redis;

image-20211012152951449

@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;
        }
    }
}

相關文章