在前端展示时,有时需要将名字、电话号码、身份证等敏感信息过滤展示(脱敏),这种一般需要后端处理,提前将敏感信息过滤换成**的字样。
第一种方式是在每个页面展示时,去过滤,但是需要改动的地方非常多。实用性不强;
第二种方式是采用面向切面编程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 标记这个注解是继承于哪个注解类
若需要完整的代码,请点【推荐】,然后留言。或觉得博文不错 也请推荐留言,感谢你的支持。