在前端展示時,有時需要將名字、電話號碼、身份證等敏感信息過濾展示(脫敏),這種一般需要后端處理,提前將敏感信息過濾換成**的字樣。
第一種方式是在每個頁面展示時,去過濾,但是需要改動的地方非常多。實用性不強;
第二種方式是采用面向切面編程AOP相類似的方式,只需要寫一個方法,然后在方法上加一個自定義類注解,在過濾的屬性上加上類型注解就解決。
這里主要講第二種方式 實現步驟為:
①定義脫敏的類型
②自定義類或方法的注解和字段的注解
③定義返回數據的格式
④實現脫敏的規則
⑤在字段和類上使用注解,聲明脫敏的字段
⑥測試調用接口 獲得結果
本博客涉及到的知識:
①如何自定義注解,以及各注解代表的含義
②了解特定注解@ControllerAdvice的含義以及接口ResponseBodyAdvice的機制
③反射獲取數據,解析數據
④實現脫敏的邏輯
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 標記這個注解是繼承於哪個注解類
若需要完整的代碼,請點【推薦】,然后留言。或覺得博文不錯 也請推薦留言,感謝你的支持。