赞
踩
1.自定义注解类
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingSerializer.class)
public @interface DataMask {
DataMaskEnum function() default DataMaskEnum.EMAIL;
}
2.定义策略枚举
public enum DataMaskEnum { /** * Email sensitive type. */ EMAIL(s -> DataMaskContentUtil.getMaskToEmail(s)); /** * 成员变量 是一个接口类型 */ private Function<String, String> function; DataMaskEnum(Function<String, String> function) { this.function = function; } public Function<String, String> function() { return this.function; } }
public class DataMaskingSerializer extends JsonSerializer<String> implements ContextualSerializer { private DataMaskEnum dataMaskEnum; @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(dataMaskEnum.function().apply(value)); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { DataMask annotation = property.getAnnotation(DataMask.class); if (Objects.nonNull(annotation)&&Objects.equals(String.class, property.getType().getRawClass())) { this.dataMaskEnum = annotation.function(); return this; } return prov.findValueSerializer(property.getType(), property); } }
4.Jackson 序列化触发工具类
public class JsonUtils { private static final Logger log = Logger.getLogger(JsonUtils.class); final static ObjectMapper objectMapper; static { objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); } public static ObjectMapper getObjectMapper() { return objectMapper; } /** * JSON串转换为Java泛型对象 * * @param <T> * @param jsonString JSON字符串 * @param tr TypeReference,例如: new TypeReference< List<FamousUser> >(){} * @return List对象列表 */ public static <T> T toGenericObject(String jsonString, TypeReference<T> tr) { if (jsonString == null || "".equals(jsonString)) { return null; } else { try { return (T) objectMapper.readValue(jsonString, tr); } catch (Exception e) { log.warn(jsonString); log.warn("json error:" + e.getMessage()); } } return null; } /** * Json字符串转Java对象 * * @param jsonString * @param c * @return */ public static Object toObject(String jsonString, Class<?> c) { if (jsonString == null || "".equals(jsonString)) { return ""; } else { try { return objectMapper.readValue(jsonString, c); } catch (Exception e) { log.warn("json error:" + e.getMessage()); } } return ""; } /** * Java对象转Json字符串 * * @param object Java对象,可以是对象,数组,List,Map等 * @return json 字符串 */ public static String toJson(Object object) { String jsonString = ""; try { jsonString = objectMapper.writeValueAsString(object); } catch (Exception e) { e.printStackTrace(); log.warn("json error:" + e.getMessage()); } return jsonString; } }
5.注解引用,新建实体对象 并且在属性上引用注解并指定策略
@Data
public class DataMaskDTO {
@DataMask(function=DataMaskEnum.EMAIL)
private String email;
}
6.验证结果,新建实体类并调用 json工具类就可以了
@Slf4j
public class DataMaskTest {
@Test
public void maskCommitDTO(){
DataMaskDTO dataMaskDTO = new DataMaskDTO();
dataMaskDTO.setEmail("1234444@163.com");
log.info("jsonMaskContent :{}", JsonUtils.toJson(dataMaskDTO));
log.info("old : {}",dataMaskDTO);
}
}
首先看一下JSONField元注解的源码,这里只需要用到序列化
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) public @interface JSONField { /** * config encode/decode ordinal * @since 1.1.42 * @return */ int ordinal() default 0; String name() default ""; String format() default ""; boolean serialize() default true; boolean deserialize() default true; SerializerFeature[] serialzeFeatures() default {}; Feature[] parseFeatures() default {}; String label() default ""; /** * @since 1.2.12 */ boolean jsonDirect() default false; /** * Serializer class to use for serializing associated value. * * @since 1.2.16 */ Class<?> serializeUsing() default Void.class; /** * Deserializer class to use for deserializing associated value. * * @since 1.2.16 */ Class<?> deserializeUsing() default Void.class; /** * @since 1.2.21 * @return the alternative names of the field when it is deserialized */ String[] alternateNames() default {}; /** * @since 1.2.31 */ boolean unwrapped() default false; /** * Only support Object * * @since 1.2.61 */ String defaultValue() default ""; }
2.以上注解属性还有个序列化(serializeUsing)和反序列化(deserializeUsing)
这两个属性会在目标对象执行 JSON.toJSONString时触发,从而属性值进行序列化处理,JSON.parseObject 触发反序列化操作。
由于业务场景比较复杂,正则不支持,所以我们需要再序列化里加入自定义格式化内容处理。
1.新建一个Java类,实现ObjectSerializer接口,重写write 方法,serializer 是序列化对象,object 是属性值,这里我们只需要关注这两个属性,格式化object 内容再写入serializer中就可以了
public class EmailMaskSerializer implements ObjectSerializer {
@Override
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
//处理object内容
Object o = MaskUtil.pase(object);;
serializer.write(o);
}
}
2.调用方式 新建实体类 在字段上加上JSONField注解,并且serializeUsing 指向 我们写好的内容处理类EmailMaskSerializer
@Data
public class DataMaskDTO {
@JSONField(name = "email",serializeUsing = EmailMaskSerializer.class)
private String email;
}
3.定义脱敏工具类
public class MaskUtils extends StringUtils { /** * 除邮箱格式后缀加密长度 */ public static final int EMAIL_LENGTH = 11; /** * 除邮箱格式后缀加密长度 */ public static final int EMAIL_MASK_HANDE = 3; /** * 除邮箱格式后缀加密长度 */ public static final int NAME_MASK_LENGTH = 3; /** * 返回用户名脱敏串 * * @param name * @return */ public static String getMaskToName(String name) { if (isBlank(name) || name.length() == 1) { return name; } int index = name.length() > 2 ? 1 : 0; return getMaskStr(name, 1, index); } /** * 返回用户名脱敏串 * * @param phone * @return */ public static String getMaskToPhone(String phone) { if (isBlank(phone) || phone.length() == 1) { return phone; } int index = phone.length() >= 11 ? 4 : 0; return getMaskStr(phone, 3, index); } /** * 返回邮箱脱敏串 * * @param object * @return */ public static String getMaskToEmail(Object object) { if (Objects.isNull(object)) { return null; } String email = object.toString(); int emailFlagIndex = email.lastIndexOf("@"); if (isBlank(email) || emailFlagIndex < 0) { return email; } //兼容非正常邮箱脱敏 例如1@163.com 或 @163.com等 int handIndex = Math.min(emailFlagIndex, EMAIL_MASK_HANDE); int maskIndex = EMAIL_LENGTH - handIndex; String maskSb = String.format("%s%s%s", email.substring(0, handIndex), getMaskStrByLength(maskIndex), email.substring(emailFlagIndex)); return maskSb; } /** * 通用脱敏方案 * * @param str 脱敏内容 * @param first 保留原本内容前?位 * @param last 保留原本内容最后?位 * @return */ public static String getMaskStr(String str, Integer first, Integer last) { if (isBlank(str)) { return str; } StringBuffer sb = new StringBuffer(); int index = str.length() - (first + last); sb.append(str.substring(0, first)); sb.append(getMaskStrByLength(index)); sb.append(str.substring(str.length() - last)); // sb.append(getMaskToEmail(str)); return sb.toString(); } /** * 根据传入的个数返回*串字符 * * @param index * @return */ public static String getMaskStrByLength(Integer index) { if (index <= 0) { return ""; } StringBuffer maskStr = new StringBuffer(); for (int i = 0; i < index; i++) { maskStr.append("*"); } return maskStr.toString(); } }
4.验证结果:
@Slf4j
public class DataMaskTest {
@Test
public void maskCommitDTO(){
DataMaskDTO dataMaskDTO = new DataMaskDTO();
dataMaskDTO.setEmail("1234444@163.com");
String json = JSON.toJSONString(dataMaskDTO);
log.info("jsonMaskContent :{}", json);
}
}
1.自定义注解类
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataMask {
DataMaskEnum function() default DataMaskEnum.EMAIL;
}
2.定义策略枚举
public enum DataMaskEnum { /** * Email sensitive type. */ EMAIL(s -> DataMaskContentUtil.getMaskToEmail(s)); /** * 成员变量 是一个接口类型 */ private Function<String, String> function; DataMaskEnum(Function<String, String> function) { this.function = function; } public Function<String, String> function() { return this.function; } }
3.定义过滤器,实现ValueFilter过滤器,重新 process方法,在process方法中进行脱敏策略处理
@Component @Slf4j public class ValueMaskFilter implements ValueFilter { @Override public Object process(Object o, String name, Object value) { try { if (Objects.isNull(value) || !(value instanceof String)) { return value; } try { Field field = o.getClass().getDeclaredField(StringUtil.underlineToCamel(name)); log.info("name :{}, value :{}", name, value); DataMask dataMask; if (String.class != field.getType() || (dataMask = field.getAnnotation(DataMask.class)) == null) { return value; } String valueStr = value.toString(); DataMaskEnum dataMaskEnum = dataMask.function(); switch (dataMaskEnum) { case EMAIL: return MaskUtils.getMaskToEmail(valueStr); case USERNAME: return MaskUtils.getMaskToName(valueStr); default: } } catch (NoSuchFieldException e) { return value; } return value; } catch (Exception e) { return value; } } }
3.创建WebConfig配置类,将fastjson过滤器注册到容器中,这里只要关注 configureMessageConverters方法
@Configuration public class WebConfig extends WebMvcConfigurerAdapter { static { //全局配置关闭 Fastjson 循环引用 JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.DisableCircularReferenceDetect.getMask(); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new ByteArrayHttpMessageConverter()); converters.add(new StringHttpMessageConverter()); final List<MediaType> supplyMediaTypes = Arrays.stream( new MediaType[]{MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_PLAIN, MediaType.ALL, MediaType.valueOf("application/vnd.git-lfs+json")}).collect(Collectors.toList()); FastJsonHttpMessageConverter4 fastJsonHttpMessageConverter4 = new FastJsonHttpMessageConverter4(); fastJsonHttpMessageConverter4.setDefaultCharset(defaultCharset); fastJsonHttpMessageConverter4.setSupportedMediaTypes(supplyMediaTypes); final FastJsonConfig config = new FastJsonConfig(); config.setCharset(defaultCharset); config.setSerializerFeatures(Constants.SERIALIZER_FEATURES); config.getSerializeConfig().put(Object.class, DataMaskEmailSerializer.DATA_MASK_EMAIL_SERIALIZER); config.getSerializeConfig().put(Date.class, ForceDateCodec.INSTANCE); //自定义脱敏过滤器 config.setSerializeFilters(new ValueMaskFilter()); System.out.println(config.getSerializeConfig().get(String.class)); fastJsonHttpMessageConverter4.setFastJsonConfig(config); converters.add(fastJsonHttpMessageConverter4); super.configureMessageConverters(converters); } @Override public Validator getValidator() { return null; } @Override public MessageCodesResolver getMessageCodesResolver() { return null; } private Charset defaultCharset = Charset.forName("UTF-8"); }
1.自定义注解类
@Target({ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataMask { DataMaskEnum function() default DataMaskEnum.EMAIL; } 2.定义策略枚举 public enum DataMaskEnum { /** * Email sensitive type. */ EMAIL(s -> DataMaskContentUtil.getMaskToEmail(s)); /** * 成员变量 是一个接口类型 */ private Function<String, String> function; DataMaskEnum(Function<String, String> function) { this.function = function; } public Function<String, String> function() { return this.function; } }
3.定义spring aop拦截器,针对前端请求进行拦截处理,这里着重注意@AfterReturning,在返回前端之前做处理
这里需要注意handleMaskValue方法,递归时对于对象类型判断,可能存在内存泄露的风险,所以在使用的时候根据情况对 UN_HANDLE_FILED_TYPE_LIST 集合进行补全即可。
package com.alibaba.force.api.component.aop; import com.alibaba.force.api.component.authentication.aop.InterceptorOrder; import com.alibaba.force.common.diamond.DiamondConfigConstants; import com.alibaba.force.common.diamond.DiamondConfigService; import com.alibaba.force.common.enums.AppDeployEnvEnum; import com.alibaba.force.common.model.PageResult; import com.alibaba.force.common.util.CollectionUtils; import com.alibaba.force.common.util.PropertyUtils; import com.alibaba.force.common.util.mask.DataMask; import com.alibaba.force.common.util.mask.DataMaskContentUtil; import com.alibaba.force.common.util.mask.DataMaskEnum; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.springframework.core.Ordered; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; /** * 接口返回参数脱敏拦截器 * * @author jiangzhiming */ @Slf4j @Aspect @Service @Component public class DataSourceMaskValueInterceptor implements Ordered { //递归深度 以防内存溢出风险 private static int num = 20; private static int startNum = 1; @Pointcut(value = "execution(* com.alibaba.force..api..*(..)) && @annotation(com.alibaba.force.common.util.mask.DataMask)") public void dataSourceMaskValuePointCutRest() { } /** * 需要略过的字段类型(基本数据类型) */ public static final List<Object> UN_HANDLE_FILED_TYPE_LIST = Arrays.asList( int.class, float.class, double.class, long.class, short.class, byte.class, boolean.class, char.class); /** * 脱敏开关 true 开启 false 关闭 */ public Boolean getMaksEnabled() { Optional<String> maskEnabled = DiamondConfigService.getConfig(DiamondConfigConstants.MASK_ENABLED); return maskEnabled.isPresent() && Boolean.parseBoolean(maskEnabled.get()); } /** * 后置处理返回参数 */ @AfterReturning(value = "dataSourceMaskValuePointCutRest()", returning = "result") public Object afterReturning(JoinPoint joinPoint, Object result) throws Throwable { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); try { DataMask dataMask = method.getAnnotation(DataMask.class); log.info("dataSourceMaskValuePointCutRest: methodSignatureName={},dataMask:{},getMaksEnabled:{}", method.getName(), dataMask, getMaksEnabled()); if (getMaksEnabled() && dataMask != null && checkedAppDeployEnv()) { if (result instanceof ResponseEntity) { ResponseEntity resultEntity = (ResponseEntity) result; Object resultData = resultEntity.getBody(); handleObject(startNum, resultData); } } return joinPoint.getThis(); } catch (Exception e) { return joinPoint.getThis(); } finally { restSum(); } } /** * 对象脱敏处理 * * @param resultData */ public void handleObject(int thisNum, Object resultData) { try { //递归最大深度时 直接终止 if (thisNum >= num) { return; } if (resultData instanceof List) { List<Object> resultList = (List) resultData; if (CollectionUtils.isNotEmpty(resultList)) { for (Object obj : resultList) { handleMaskValue(sumByAdd(thisNum), obj); } } } else if (resultData instanceof PageResult) { //分页情况 PageResult pageResult = (PageResult) resultData; List<Object> resultList = (List<Object>) pageResult.getList(); if (CollectionUtils.isNotEmpty(resultList)) { handleObject(sumByAdd(thisNum), resultList); } } else { //处理单个对象情况 handleMaskValue(sumByAdd(thisNum), resultData); } } catch (Exception e) { } finally { sumBySubtract(thisNum); } } /** * 递归对象属性 处理属性复杂的对象 * * @param obj * @param */ public void handleMaskValue(int thisNum, Object obj) { try { if (Objects.isNull(obj) || isBasicDataType(obj)) { return; } if (obj instanceof Map) { Map<String, Object> objectMap = (Map<String, Object>) obj; for (String key : objectMap.keySet()) { Object value = objectMap.get(key); if (value instanceof String) { objectMap.put(key, getMaskValue(value.toString(), getMaskType(key))); } else { handleObject(sumByAdd(thisNum), value); } } } else if (obj instanceof List) { List<Object> resultList = (List) obj; if (CollectionUtils.isNotEmpty(resultList)) { handleObject(sumByAdd(thisNum), resultList); } } else { DataMask dataMask; List<Field> fields = getFields(obj.getClass()); for (Field field : fields) { field.setAccessible(true); if (String.class == field.getType()) { if ((dataMask = field.getAnnotation(DataMask.class)) != null) { //如果属性类型是时间类型,取出属性的值 String valueStr = (String) field.get(obj); DataMaskEnum dataMaskEnum = dataMask.function(); field.set(obj, getMaskValue(valueStr, dataMaskEnum)); } continue; } //基本类型 不包含字符串 为class对象时 Object object = field.get(obj); if (!isBasicDataType(object)) { handleObject(sumByAdd(thisNum), object); } } } } catch (Exception e) { } } /** * 判断对象是否为基本数据类型 * * @param clazz * @return */ public boolean isBasicDataType(Object clazz) { //是否包装类 这个需要单独判断 防止死循环造成内存溢出 if (clazz == Integer.class || clazz == Float.class || clazz == Double.class || clazz == Long.class || clazz == Short.class || clazz == Byte.class || clazz == Boolean.class || clazz == Character.class || clazz == Date.class || clazz == java.sql.Date.class || clazz == Enum.class) { return true; } if (clazz instanceof Integer || clazz instanceof Float || clazz instanceof Double || clazz instanceof Long || clazz instanceof Short || clazz instanceof Byte || clazz instanceof Boolean || clazz instanceof Character || clazz instanceof Date || clazz instanceof java.sql.Date || clazz instanceof Enum) { return true; } if (clazz instanceof Logger) { return true; } return UN_HANDLE_FILED_TYPE_LIST.contains(clazz); } /** * 类型为map时 需要进行字段模糊匹配,以防遗漏,这里目前还有一定缺陷,不能做到区别脱敏,目前是全脱敏 * * @param fieldName * @return */ public DataMaskEnum getMaskType(String fieldName) { if (StringUtils.isNotBlank(fieldName)) { if (fieldName.toLowerCase().contains(DataMaskEnum.EMAIL.name().toLowerCase())) { return DataMaskEnum.EMAIL; } } return null; } /** * 根据脱敏类型获取脱敏内容 * * @param value * @param dataMaskEnum * @return */ public String getMaskValue(String value, DataMaskEnum dataMaskEnum) { if (dataMaskEnum != null) { switch (dataMaskEnum) { case EMAIL: return DataMaskContentUtil.getMaskToEmail(value); default: return value; } } return value; } /** * 获取对象的所以字段 包括继承的父类字段 * * @param c * @return */ private List<Field> getFields(Class c) { List<Field> fields = new ArrayList<>(); while (c != null) { fields.addAll(Arrays.asList(c.getDeclaredFields())); c = c.getSuperclass(); } return fields; } private static int sumByAdd(int n) { return n == startNum ? 1 : ++n; } private static int sumBySubtract(int n) { return n == num ? num : --n; } private static void restSum() { num = 20; } /** * 校验部署环境 * * @return */ private boolean checkedAppDeployEnv() { AppDeployEnvEnum currentDeployEnv = PropertyUtils.getAppDeployEnv(); //获取当前部署环境,针对特定的环境进行处理 if (AppDeployEnvEnum.ALIYUN.equals(currentDeployEnv)) { return true; } return false; } @Override public int getOrder() { return InterceptorOrder.MASK_DATA_SOURCE; } }
最后的方案采用
第一种方案,对fastjson不起作用,并且带有一定的侵入,所以不采取该方案。
第二种方案,可能内部存在序列化和反序列化场景,会影响内部的业务,故不采用该方案。
第三种方案,DTO层比较混乱,而且过滤器在webconfig注册后未生效,原因还在排查中。
第四种方案, 增加aop拦截器,在拦截器中的后置处理器中,对返回的参数属性进行解析,带有自定义注解的参数进行拦截,这里兼容了层级复杂多类型的数据结构,不过递归时可能存在内存溢出风险,只要把对象类型的判断处理好就能避免内存泄漏情况。这里为了方便控制脱敏效果,增加了脱敏开关和目标方法进行双重控制,降低对不需要脱敏的结果造成干扰,同时也省去不必要的处理。还需要注意的是,map类型参数无法做到指定脱敏,这里对map key进行了遍历模糊匹配,如果匹配到策略,则会采用脱敏策略对参数进行处理。
目前采取第四种方案。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。