前言
Jackson 是用來序列化和反序列化 json 的 Java 的開源框架。Spring MVC 的默認 json 解析器便是 Jackson。與其他 Java 的 json 的框架 Gson 等相比, Jackson 解析大的 json 文件速度比較快;Jackson 運行時占用內存比較低,性能比較好;Jackson 有靈活的 API,可以很容易進行擴展和定制。
在有些業務場景下,后臺保存的敏感數據不適宜在前端(或傳輸)直接展示,需要將敏感數據脫敏后返回,比較簡單的方式是自定義Jackson序列化器進行數據脫敏。
自定義注解
package com.cube.share.jackson.annotation;
import com.cube.share.jackson.serializer.SensitiveDataSerializer;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* @author poker.li
* @date 2021/8/16 15:44
* <p>
* 需要脫密的字段注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@Target(ElementType.FIELD)
@JsonSerialize(using = SensitiveDataSerializer.class)
public @interface SensitiveData {
/**
* 默認的字段脫敏替換字符串
*/
String DEFAULT_REPLACE_STRING = "*";
/**
* 脫敏策略
*/
Strategy strategy() default Strategy.TOTAL;
/**
* 脫敏長度,在Strategy.TOTAL策略下忽略該字段
*/
int length() default 0;
/**
* 脫敏字段替換字符
*/
String replaceStr() default DEFAULT_REPLACE_STRING;
enum Strategy {
/**
* 全部
*/
TOTAL,
/**
* 從左邊開始
*/
LEFT,
/**
* 從右邊開始
*/
RIGHT
}
}
這個注解比較簡單,注釋很詳細就不贅述了。
自定義序列化器
package com.cube.share.jackson.serializer;
import com.cube.share.jackson.annotation.SensitiveData;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
/**
* @author poker.li
* @date 2021/8/16 19:32
* <p>
* 脫敏字段序列化器
*/
public class SensitiveDataSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveData sensitiveData;
public SensitiveDataSerializer(SensitiveData sensitiveData) {
this.sensitiveData = sensitiveData;
}
public SensitiveDataSerializer() {
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (StringUtils.isBlank(value)) {
gen.writeString(value);
return;
}
if (sensitiveData != null) {
final SensitiveData.Strategy strategy = sensitiveData.strategy();
final int length = sensitiveData.length();
final String replaceString = sensitiveData.replaceStr();
gen.writeString(getValue(value, strategy, length, replaceString));
} else {
gen.writeString(value);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
SensitiveData annotation = property.getAnnotation(SensitiveData.class);
if (annotation != null) {
return new SensitiveDataSerializer(annotation);
}
return this;
}
private String getValue(String rawStr, SensitiveData.Strategy strategy, int length, String replaceString) {
switch (strategy) {
case TOTAL:
return rawStr.replaceAll("[\\s\\S]", replaceString);
case LEFT:
return replaceByLength(rawStr, length, replaceString, true);
case RIGHT:
return replaceByLength(rawStr, length, replaceString, false);
default:
throw new IllegalArgumentException("Illegal Sensitive Strategy");
}
}
private String replaceByLength(String rawStr, int length, String replaceString, boolean fromLeft) {
if (StringUtils.isBlank(rawStr)) {
return rawStr;
}
if (rawStr.length() <= length) {
return rawStr.replaceAll("[\\s\\S]", replaceString);
}
if (fromLeft) {
return getSpecStringSequence(length, replaceString) + rawStr.substring(length);
} else {
return rawStr.substring(0, length) + getSpecStringSequence(length, replaceString);
}
}
private String getSpecStringSequence(int length, String str) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
stringBuilder.append(str);
}
return stringBuilder.toString();
}
}
通過自定義序列化器改變使用注解 @SensitiveData
字段在序列化時的表現,將其全部或部分使用特殊字符替換,從而達到脫敏的效果。
這里有必要提一下ContextualSerializer
這個接口,ContextualSerializer
內只有一個方法createContextual
,自定義JsonSerializer
實現ContextualSerializer
后,該序列化器可以根據所要序列化屬性(實體類的屬性)的類型或者配置的注解類型來改變該屬性的序列化行為;方法createContextual
的聲明如下:
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException;
參數SerializerProvider prov
表示序列化器提供者,用于獲取序列化配置或者其他序列化器,參數BeanProperty property
表示代表這個屬性的方法或者字段,用于獲取要序列化的值。該方法的返回結果是一個序列化器,根據所要實現的序列化行為來決定是返回當前序列化器還是新建一個序列化器,從而改變序列化時的行為。
因此,在實現自定義序列化器時,可以通過判斷某一字段上是否具有 @SensitiveData
,如果有,獲取該注解的屬性并新建一個序列化器,改變當前字段在序列化時的行為。
測試
實體類聲明如下:
package com.cube.share.jackson.entity;
import com.cube.share.jackson.annotation.SensitiveData;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author poker.li
* @date 2021/8/16 14:16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person {
private Long id;
private String name;
private String address;
private Integer age;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createAt;
private LocalDateTime updateAt;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate lastLoginDate;
@SensitiveData(strategy = SensitiveData.Strategy.LEFT, length = 6, replaceStr = "*")
private String mobile;
@JsonUnwrapped
private Role role;
}
其中,字段手機號的前六位需要進行脫敏處理,并且使用'*'填充前六位。
package com.cube.share.jackson.controller;
import com.cube.share.base.templates.ApiResult;
import com.cube.share.jackson.entity.Person;
import com.cube.share.jackson.entity.Role;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author poker.li
* @date 2021/8/16 14:17
*/
@RestController
@RequestMapping("/person")
@Slf4j
public class PersonController {
@GetMapping("/detail/{id}")
public ApiResult detail(@PathVariable("id") Long id) {
Role role = new Role();
role.setId(12L);
role.setRoleName("管理員");
return ApiResult.success(Person.builder()
.id(id)
.age(18)
.name("lis")
.address("北京")
.createAt(LocalDateTime.now())
.updateAt(LocalDateTime.now())
.lastLoginDate(LocalDate.now())
.mobile("15824984456")
.role(role)
.build());
}
@PostMapping("/save")
public ApiResult save(@RequestBody Person person) {
log.info("用戶信息: {}", person);
return ApiResult.success();
}
}
調用詳情接口響應如下:
{
code: 200,
msg: null,
data: {
id: 1,
name: "lis",
address: "北京",
age: 18,
createAt: "2021-08-18 21:53:04",
updateAt: "2021-08-18T21:53:04.346",
lastLoginDate: "2021-08-18",
mobile: "******84456",
roleName: "管理員",
desc: null,
roleId: 12
}
}
可以看出手機號返回值已經脫敏。