在前端展示時,有時需要將名字、電話號碼、身份證等敏感資訊過濾展示(脫敏),這種一般需要後端處理,提前將敏感資訊過濾換成**的字樣。
第一種方式是在每個頁面展示時,去過濾,但是需要改動的地方非常多。實用性不強;
第二種方式是通過面向切面程式設計AOP的方式,只需要寫一個方法,然後在方法上加一個自定義註解就解決。
這裡主要講第二種方式
1.自定義註解
宣告一個列舉脫敏型別
/** * 資料脫敏型別 */ public enum DesensitizeType { NAME, // 名稱 ID_CARD_18, //身份證 18 EMAIL,//email MOBILE_PHONE; //手機號 }
宣告脫敏的欄位 的註解(用在欄位上)
/** * 標記欄位 使用何種策略來脫敏 */ @Documented @Retention(value = RetentionPolicy.RUNTIME) @Target(value = {ElementType.FIELD}) @Inherited public @interface Desensitize { DesensitizeType type(); }
宣告脫敏的方法或類的註解
/** * 標記在類、方法上,是否需要脫敏 */ @Documented @Retention(value = RetentionPolicy.RUNTIME) @Target(value={ElementType.METHOD, ElementType.TYPE}) @Inherited //說明子類可以繼承父類中的該註解 public @interface DesensitizeSupport { }
2.實現資料脫敏
定義響應的物件格式
/** * 響應實體 */ public class ResResult { /** * 編碼 */ private String code; /** * 提示資訊 */ private String message; /** * 資料 */ private Object data; //get //set... }
資料的model,對要脫敏的欄位加註解@Desensitize和脫敏型別DesensitizeType
public class UserModel implements Serializable { /** * 姓名 */ @Desensitize(type = DesensitizeType.NAME) private String name; private Integer age; private String desc; /** * 電話號碼 */ @Desensitize(type = DesensitizeType.MOBILE_PHONE) private String telNumber; //get //set... }
定義一個controller層,在類或者方法上加脫敏註解@DesensitizeSupport 表示該類或方法支援脫敏
@RestController @RequestMapping("/test") @DesensitizeSupport public class UserController { @Autowired private IUserService iUserService; @GetMapping(value = "/listuser") public ResResult testHello() { ResResult result = new ResResult(); List<UserModel> list = iUserService.listUser(); result.setData(list); return result; } }
Service層
@Service public class UserServiceImpl implements IUserService { @Override public List<UserModel> listUser() { UserModel user = new UserModel(); user.setName("李四"); user.setAge(123); ArrayList<UserModel> list = new ArrayList<>(); list.add(user); return list; } }
有了以上的部分後,還不會進行脫敏,還需要加上脫敏的具體操作。在Controller層執行了return語句後,在返回到前端之前,會執行如下程式碼進行脫敏:
/** * 脫敏工具類 */ public class DesensitizeUtils { public static void main(String[] args) { String name = "李明"; System.out.println(repVal(name, 1, 1)); } public static String dataMasking(DesensitizeType type, String oldValue) { String newVal = null; switch (type) { case NAME: newVal = repVal(oldValue, 1, 1); break; case ID_CARD_18: break; case EMAIL: break; case MOBILE_PHONE: break; } return newVal; } /** * 字元替換 * @param val * @param beg * @param end * @return */ public static String repVal(String val, int beg, int end) { if (StringUtils.isEmpty(val)) { return null; } String name = val.substring(0, beg); int length = val.length(); if (length > 2 && length > end) { return name + "**" + val.substring(length-end); } else if (length == 2) { return name + "*"; } return val; } }
/** * 統一處理 返回值/響應體 */ @ControllerAdvice public class DesensitizeResponseBodyAdvice implements ResponseBodyAdvice<Object> { private final static Logger logger = LoggerFactory.getLogger(DesensitizeResponseBodyAdvice.class); @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { AnnotatedElement annotatedElement = returnType.getAnnotatedElement(); //1.首先判斷該方法是否存在@DesensitizeSupport註解 //2.判斷類上是否存在 Method method = returnType.getMethod(); DesensitizeSupport annotation = method.getAnnotation(DesensitizeSupport.class); DesensitizeSupport clazzSup = method.getDeclaringClass().getAnnotation(DesensitizeSupport.class); return annotation != null || clazzSup != null; } /** * * @param body * @param returnType * @param selectedContentType * @param selectedConverterType * @param request * @param response * @return */ @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { logger.debug("Test ResponseBodyAdvice ==> beforeBodyWrite:" + body.toString() + ";" + returnType); Class<?> childClazz = body.getClass(); Field childField = null; List filedValue = null; try { //獲取資料 childField = childClazz.getDeclaredField("data"); //設定可訪問 childField.setAccessible(true); Object objs = childField.get(body); if (!(objs instanceof List)) { logger.debug("這是不是List型別"); return body; } filedValue = (List) objs; //對值進行脫敏 for (Object obj : filedValue) { dealValue(obj); } } catch (NoSuchFieldException e) { logger.error("未找到資料; message:" + e.getMessage()); e.printStackTrace(); } catch (IllegalAccessException e) { logger.error("處理異常; message:" + e.getMessage()); e.printStackTrace(); } return body; } public void dealValue(Object obj) throws IllegalAccessException { Class<?> clazz = obj.getClass(); //獲取奔雷和父類的屬性 List<Field> fieldList = getAllFields(clazz); for (Field field : fieldList) { //獲取屬性上的註解 Desensitize annotation = field.getAnnotation(Desensitize.class); if (annotation == null) { continue; } Class<?> type = field.getType(); //判斷屬性的型別 if (String.class != type) { //非字串的型別 直接返回 continue; } //獲取脫敏型別 判斷是否脫敏 DesensitizeType annotType = annotation.type(); field.setAccessible(true); String oldValue = (String) field.get(obj); String newVal = DesensitizeUtils.dataMasking(annotType, oldValue); field.set(obj, newVal); } } /** * 獲取所有的欄位(包括父類的) * @param clazz * @return */ public List<Field> getAllFields(Class<?> clazz) { List<Field> fieldList = new ArrayList<>(); while (clazz != null) { Field[] declaredFields = clazz.getDeclaredFields(); fieldList.addAll(Arrays.asList(declaredFields)); //獲取父類,然後獲取父類的屬性 clazz = clazz.getSuperclass(); } return fieldList; } }
3.結果
響應的結果,我們期待的兩個字的名稱【李四】會【李*】,三個字或三個以上的【李小明】會變成【李**明】(規則可自己進行設定)
注:在Controller層執行了return語句後,在返回到前端之前 會執行DesensitizeResponseBodyAdvice類中的supports和beforeBodyWrite方法,其中在類上有一個很重要的註解@ControllerAdvice和很重要的介面ResponseBodyAdvice,這兩個結合在一起,就具有統一處理返回值/響應體的功能。(相當於一個攔截器)
①@ControllerAdvice註解,這是一個Controller的增強型註解,可以實現三方面的功能:
- 全域性異常處理
- 全域性資料繫結
- 全域性資料預處理
②介面ResponseBodyAdvice
繼承了該介面,需要實現兩個方法,supports和beforeBodyWrite方法。在supports方法返回為true後,才會執行beforeBodyWrite方法。其中beforeBodyWrite方法中的body就是響應物件response中的響應體,可以對響應體做統一的處理,比如加密、簽名、脫敏等操作。
這裡簡單講解一下其中的註解:
使用【@interface】是自定義一個註解,通常自定義的註解上面還有其他註解,如以下幾個:
@Documented 表示標記這個註解是否會包含在文件中
@Retention 標識這個註解怎麼儲存,有三種狀態,value = RetentionPolicy.RUNTIME 表示不僅保留在原始碼中,也保留在class中,並且在執行時可以訪問;
SOURCE 表示只保留在原始碼中,當在class檔案中時被遺棄;CLASS 表示保留在class檔案中,但jvm載入class檔案時被遺棄。
@Target 標註這個註解屬於Java哪個成員,通常有屬類、方法;欄位;引數;包等
@Inherited 標記這個註解是繼承於哪個註解類
。。。