遇到个需求就是实现数据脱敏,本来想抄另外一个项目的,但是发现另外一个项目的选型存在瑕疵:
另一个项目的脱敏使用的是ResponseAdvice,在请求结束后进行拦截。这个有什么瑕疵呢?
瑕疵点在于需要递归遍历请求体的每个属性寻找对应的注解,首先递归就是一项耗费资源的功夫,再者还会对特殊部分数据做特殊递归处理(如集合类型以及map类型),仅对其中的两者有特殊处理。那假如自定义一种数据结构呢?那就要不断的在代码里往下写增加特殊处理,显然这么做是有瑕疵的。
于是经过查找挑选后,确定使用jackson的序列化的注解@JsonDeserialize 进行处理,同时仅处理在请求响应中的数据,因为常规来说脱敏也只是对请求接口而言的仅仅是返回客户端用到的,自己后台定时任务处理数据明显不需要。
贴上代码
- 先自定义脱敏类型
/**
1. 脱敏类型
*/
public enum SensitiveType {
// 用户名
USER_NAME,
// 身份证号
PAPERWORK_NO,
// 手机号
MOBILE_PHONE,
// 地址
HOME_ADDRESS,
// 姓名
REAL_NAME
}
- 设计一个根据脱敏类型做不同处理的脱敏策略处理器
根据注解里传来的type值new出不同种类的(手机号,邮箱,身份证…)SensitiveStrategyHandler,调用其desensitization方法做具体的脱敏数据处理
/**
* 统一脱敏策略处理
*/
@AllArgsConstructor
public class SensitiveStrategyHandler {
private SensitiveType type;
/**
* 处理脱敏
*
* @param entity 需要脱敏的数据
* @return 脱敏后的数据
*/
public String desensitization(String entity) {
if (StringUtils.isEmpty(entity)) {
return entity;
}
switch (type){
case USER_NAME: {
// 策略为中间两次
int i = entity.length() / 2 / 2;
return this.replaceCharacters(entity,i,entity.length()-i,'*');
}
case PAPERWORK_NO: {
// 策略为除前4位与后4位显示
return this.replaceCharacters(entity,4,entity.length()-4,'*');
}
case MOBILE_PHONE: {
// 将中间4位数进行*代替
int start = (entity.length() - 4)/2;
return this.replaceCharacters(entity,start,entity.length()-start-1,'*');
}
case HOME_ADDRESS: {
// 长度的1/3之后开始脱敏
int start = entity.length() / 3;
return this.replaceCharacters(entity,start,entity.length(),'*');
}
case REAL_NAME: {
// 将姓后面的字用*代替
return this.replaceCharacters(entity,1,entity.length(),'*');
}
}
return entity;
}
/**
* 将字符串开始位置到结束位置变成特定的符号
* @param input 目标字符串
* @param start 开始位置
* @param end 结束位置
* @param symbol 需要变成的符号
* @return 处理结果
*/
private String replaceCharacters(String input,int start,int end,char symbol){
if (input == null || start >= end || input.length() < end) {
return input;
}
// 差值,需要补的symbol个数
int num = end - start;
StringBuilder builder = new StringBuilder();
for (int i = 0 ; i < num ; i++){
builder.append(symbol);
}
// 获取原始字符串的前start位
String prefix = input.substring(0, start);
// 获取字符串中第end位及以后的字符
String suffix = input.substring(end);
// 拼接替换后的字符串
return prefix + builder.toString() + suffix;
}
}
- 加上策略,
使其做到根据type的值new出不同类型的处理器做不同处理
/**
* 脱敏处理策略类
*/
public class SensitiveStrategy {
/**
* 具体使用那个策略
* @param type
* @param entity
* @return
*/
public static String desensitization(SensitiveType type, String entity){
SensitiveStrategyHandler sensitiveService = new SensitiveStrategyHandler(type);
return sensitiveService.desensitization(entity);
}
}
- 设计一个注解Sensitive
加上注解jackson的注解@JacksonAnnotationsInside,类型即为第1步定义的枚举类SensitiveType 。指定自定义的序列化器SensitiveJsonSerializer.class,先讲序列化JsonSerialize,JsonDeserialize先不管后面会讲到为何这么用。
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
@JsonDeserialize(using = SensitiveJsonDeserializer.class)
@Target(ElementType.FIELD)// 修饰在属性上
@Retention(RetentionPolicy.RUNTIME)// 运行时动态获取
public @interface Sensitive {
// 脱敏类型
SensitiveType type();
}
- 自定义序列化器SensitiveJsonSerializer
当数据返回给前端的时候,会将数据进行序列化,所以需要写序列化器
首先是写下面的createContextual方法的逻辑,这个方法主要是判断啥时候跳进上面serialize方法里的。那么这里只需要在请求响应中,一般都在get请求中(获取数据的时候)进行数据的脱敏。
/**
* 数据脱敏json序列化工具
*
* @author Yjoioooo
*/
@Slf4j
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveType sensitiveType;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
// 为web上下文环境
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String method = request.getMethod();
// 需要拦截脱敏的请求:get请求或者是导出功能
boolean isRequiredSensitiveMethod = StrUtil.equals(method,"GET")
|| (request.getRequestURI().toLowerCase().contains("export")&&
(StrUtil.equals(method,"POST") || StrUtil.equals(method,"PUT")));
if(isRequiredSensitiveMethod == true && StrUtil.isNotEmpty(value)){
gen.writeString(SensitiveStrategy.desensitization(sensitiveType,value));
return;
}
}
gen.writeString(value);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if(Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())){
this.sensitiveType = annotation.type();
return this;
}
return prov.findValueSerializer(property.getType(), property);
}
}
此时这段写完后,意味着添加了@Sensitive的类,在请求的时候会经过这个序列化器进行处理,会判断是否进行脱敏处理。
- 然后自定义一个反序列化器SensitiveJsonDeserializer
这一点我看大部分文章都没有设计到,这点是用来处理什么的呢?其实在上一步第5步中已经基本可以做到在web环境里进行脱敏的功能了,没有问题。
但是!假如脱敏的数据返回给前端(比如手机号123星星123),前端提交表单给后台的时候提交的仍然是已经脱敏的数据(手机号123星星123),那么保存到数据库的时候数据不就成脱敏的数据了么?(手机号123星星123)
所以要设计这么个反序列化器。当请求为post或者是put请求的时候,将带*号的数据置成Null(一般mybatis是非null才保存,如果制定了特殊的策略需要另外修改),防止将脱敏数据写入到数据库里。
/**
* 数据脱敏json序列化工具
*
* @author Yjoioooo
*/
@Slf4j
public class SensitiveJsonDeserializer extends JsonDeserializer<String> implements ContextualDeserializer {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String text = jsonParser.getText();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if(requestAttributes != null) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String method = request.getMethod();
if(StrUtil.equals(method,"POST") || StrUtil.equals(method,"PUT")){
if(text!=null && text.contains("*")){
// 防止提交表单时吧*号带上将数据覆盖
return null;
}
}
}
return text;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if(Objects.nonNull(annotation) ){
return this;
}
return prov.findContextualValueDeserializer(property.getType(), property);
}
}
至此处理完毕
------------------分界线------------------------
2024/04/18更新:
原本在createContextual()方法里判断是否为web环境,由于这个方法仅在第一次序列化时才会执行且只会执行一次,后续不再执行。所以在这里面判断为web环境是错的,这里改为了在deserialize()以及serialize()方法中获取每次请求