情景分析
有時候在前端展示時,需要將電話號碼,身份證等敏感資訊過濾掉,顯示成 *** 的字樣;如果只是前端進行修改,那麼其實資料還是會返回,只能進行後端的修改,
疑難點:
1:並不是所有頁面都要進行模糊,比如管理員等操作不能進行模糊掉,
2:有部分匯入的功能,匯出的資料也可能需要模糊掉;新增時不能進行加密,修改有加密處理的,儲存時需要再進行恢復;
3:一些返回的欄位名字並不統一,有的叫 phoneNum,有的叫 phone,有點叫 managerPhone
4:部分前端元件比如客戶下拉框等也包含身份證資訊,同樣需要進行脫敏處理;
5:返回結果進行處理時,可能是封裝起來的物件,需要遍歷加遞迴進行處理;
實現思路:
1:許可權控制
設定頁面給操作人員新增許可權控制,哪些欄位可以顯示,哪些欄位需要進行脫敏;
2:自定義註解,將需要進行模糊型別統一封裝成一個實體,讓需要脫敏的返回型別繼承該實體,這樣可以避免每一個實體中都去新增註解,然後進行AOP程式設計,將資料進行模糊處理;
切面的處理幾乎每個都要搞,將其需要進行處理的許可權欄位放入到Redis中,修改許可權控制時刪除並更新Redis;
@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;
}
}
}