目录标题
最近业务需求,为了防止用户手机号等敏感信息泄漏,需要把手机号进行密文处理,考虑到以下两个问题
- 业务多逻辑复杂,挨个找代码改,太费劲,而且无法保证能全部找到并改完
- 不同用户需求不通,部分用户需要查看手机号,部分用户不需要查看手机号
- 脱敏跟主业务相关度低,不想代码侵入太严重
想到这个逻辑跟转json日期格式化逻辑比较相似,于是上网查找实现方案,上网查询自定义注解和json处理自定义注解的相关信息,因为Spring MVC中默认使用的是Jackson工具包,这里查找Jackson相关资料,找到如下几个Jackson提供的注解和接口
相关类用途解释
1. @JacksonAnnotationsInside
com.fasterxml.jackson.annotation.JacksonAnnotationsInside
官方: 引用文本 元注释(在其他注释上使用的注释)用于指示Jackson应该使用它拥有的元注释,而不是使用目标注释(使用此注释注释的注释)。这在创建“组合注释”时非常有用,因为它有一个容器注释,这个容器注释需要用这个注释以及它“包含”的所有注释进行注释。
通俗点说,就是自定义的注解加上这个注解,自定义的注解就会被Jackson的注解拦截器(JacksonAnnotationIntrospector)findSerializer发现拦截并处理
2. @JsonSerialize(using = SensitiveInfoSerialize.class)
com.fasterxml.jackson.databind.annotation.JsonSerialize
官方:JsonSerialize类 — 通过附加到“getter”方法或字段或值类,用于配置序列化方面的注释。注释值类时,配置用于值类的实例,但可以由更具体的注释(附加到方法或字段的注释)覆盖。
注释示例如下:
@JsonSerialize(using=MySerializer.class,
as=MySubClass.class,
typing=JsonSerialize.typing.STATIC
)
(这是as=MySubClass.class多余的,因为某些属性会阻止其他属性:特别是,“using”优先于“as”,后者优先于“typing”设置)
using属性 — 用于序列化关联值的序列化程序类。根据注释的内容,值要么是注释类的实例(可在需要类序列化程序的任何地方全局使用);或者仅用于通过getter方法序列化属性访问。
这里我的理解是JsonSerialize注解是给我自定义的注解配置序列化工具的信息,using就是设置这个注解要用哪个序列化工具。
3. JsonSerializer
com.fasterxml.jackson.databind.JsonSerializer
抽象类,该类定义ObjectMapper(以及其他链式JSONSerializer)使用提供的JsonGenerator将任意类型的对象序列化为JSON所使用的API。com.fasterxml.jackson.databind.ser.std.StdSerializer而不是此类,因为它将实现此类的许多可选方法。
注意:各种serialize方法永远不会用空值调用——调用方必须处理空值,通常是通过调用SerializerProvider.findNullValueSerializer来获取要使用的序列化程序。这也意味着在序列化空值时,不能直接使用自定义序列化程序来更改要生成的输出。
如果序列化程序是聚合序列化程序(这意味着它通过使用其他序列化程序来委托对其某些内容的处理),那么它通常还需要实现com.fasterxml.jackson.databind.ser.ResolvableSerializer,它可以找到所需的辅助序列化程序。这对于允许序列化程序的动态重写很重要;需要单独的调用接口来分离辅助序列化程序的解析(可能直接或间接地将循环链接回序列化程序本身)。
此外,为了支持每个属性的注释(根据每个属性配置序列化的各个方面),序列化程序可能需要实现com.fasterxml.jackson.databind.ser.ContextualSerializer,它允许序列化程序的专门化:调用com.fasterxml.jackson.databind.ser.ContextualSerializer.createContext传递有关属性的信息,并可以创建新配置的序列化程序来处理该特定属性。
如果同时实现了com.fasterxml.jackson.databind.ser.ResolvableSerializer和com.fasterxml.jackson.databind.ser.ContextualSerializer,则序列化程序的解析将在上下文化之前进行。
序列化处理类,用于实现具体脱敏的逻辑,把原数据变成带*的数据
4. ContextualSerializer
官方:JsonSerializer可以实现的附加接口获得一个回调,该回调可用于创建序列化程序的上下文实例,以用于处理支持的类型的属性。这对于可以通过批注配置的序列化程序很有用,或者应具有不同的行为,具体取决于要序列化的属性的类型。
就是通过回调函数实现自定义注解中的字段信息传递给自定义JsonSerializer对象的操作,比如我们这的自定义注解中存在type和permission两个字段,自定义JsonSerializer序列化工具中也存在type和permission两个字段,把注解中的两个字段值赋值给自定义JsonSerializer。
实现步骤
前提:项目使用的是SpringMVC框架,且Spring自动序列化MessageConverter使用的是默认的Jackson序列化,如果项目已改成fastjson,类比实现fastjson的自定义注解。
1.声明自定义注解 SensitiveInfo
这里我们想要区分手机号、身份证、银行卡号等多种敏感信息类型,所以使用了type;我们想要区分权限来确定是否脱敏操作,所以使用了permission字段,代码如下:
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside //让此注解可以被Jackson扫描到
@JsonSerialize(using = SensitiveInfoSerialize.class) //配置处理此注解的序列化处理类
public @interface SensitiveInfo {
/**
* 拥有此权限不加密
* @return
*/
public String permission() default "MobileEncryptPermission";
/**
* 脱敏 加密的类型
* @return
*/
public SensitiveType type() default SensitiveType.MOBILE_PHONE;
}
2.声明序列化处理类
这里实现了hasPermission()方法,来判断是否拥有指定权限。
createContextual()是实现ContextualSerializer的回调函数,根据注解内容创建SensitiveInfoSerialize并赋值。
serialize()中实现具体的脱敏密文等处理
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.Collection;
import java.util.Objects;
public class SensitiveInfoSerialize extends JsonSerializer<String>
implements ContextualSerializer
{
/**接收注解脱敏类型*/
private SensitiveType type;
/**接收可查看的权限*/
private String permission;
public SensitiveInfoSerialize() {
}
public SensitiveInfoSerialize(final SensitiveType type ,final String permission ) {
this.type = type;
this.permission = permission;
}
/**序列化的逻辑处理
*/
@Override
public void serialize(final String s, final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
if (hasPermission(permission)){
//有权限,显示原文
jsonGenerator.writeString(s);
return;
}
switch (this.type) {
case CHINESE_NAME: {
jsonGenerator.writeString(SensitiveInfoUtils.chineseName(s));
break;
}
case ID_CARD: {
jsonGenerator.writeString(SensitiveInfoUtils.idCardNum(s));
break;
}
case FIXED_PHONE: {
jsonGenerator.writeString(SensitiveInfoUtils.fixedPhone(s));
break;
}
case MOBILE_PHONE: {
jsonGenerator.writeString(SensitiveInfoUtils.mobilePhone(s));
break;
}
case ADDRESS: {
jsonGenerator.writeString(SensitiveInfoUtils.address(s, 4));
break;
}
case EMAIL: {
jsonGenerator.writeString(SensitiveInfoUtils.email(s));
break;
}
case BANK_CARD: {
jsonGenerator.writeString(SensitiveInfoUtils.bankCard(s));
break;
}
case CNAPS_CODE: {
jsonGenerator.writeString(SensitiveInfoUtils.cnapsCode(s));
break;
}
}
}
/**自定义注解被拦截后的回调函数*/
@Override
public JsonSerializer<?> createContextual(final SerializerProvider
serializerProvider,final BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) { // 为空直接跳过
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
// 非 String 类直接跳过
SensitiveInfo sensitiveInfo =
beanProperty.getAnnotation(SensitiveInfo.class);
if (sensitiveInfo == null) {
sensitiveInfo =
beanProperty.getContextAnnotation(SensitiveInfo.class);
}
if (sensitiveInfo != null) {
// 如果能得到注解,就将注解的 value 传入 SensitiveInfoSerialize
return new SensitiveInfoSerialize(sensitiveInfo.type(),sensitiveInfo.permission());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(beanProperty);
}
/**
* 判断接口是否有任意xxx,xxx权限
* 这里写的是Spring Security的判断方式,如果使用的是shiro可以参考注掉的那两行代码,如果使用的其他的认证框架,自行参考实现逻辑
* @param permission 权限符
* @return {boolean}
*/
public boolean hasPermission(String permission) {
/**Spring Securty验证方式*/
if (StringUtils.isEmpty(permission)) {
return true;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permission, x));
/**Shiro 验证方式*/
// Subject subject = SecurityUtils.getSubject();
// return subject.isPermitted(permission);
}
}
3. 类型枚举实现
public enum SensitiveType {
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份证号
*/
ID_CARD,
/**
* 座机号
*/
FIXED_PHONE,
/**
* 手机号
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 电子邮件
*/
EMAIL,
/**
* 银行卡
*/
BANK_CARD,
/**
* 公司开户银行联号
*/
CNAPS_CODE
}
4. 密文处理工具实现
import org.apache.commons.lang3.StringUtils;
public class SensitiveInfoUtils {
/**
* [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
*/
public static String chineseName(final String fullName) {
if (StringUtils.isBlank(fullName)) {
return "";
}
final String name = StringUtils.left(fullName, 1);
return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
}
/**
* [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
*/
public static String chineseName(final String familyName, final String givenName) {
if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) {
return "";
}
return chineseName(familyName + givenName);
}
/**
* [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
*/
public static String idCardNum(final String id) {
if (StringUtils.isBlank(id)) {
return "";
}
return StringUtils.left(id, 3).concat(StringUtils
.removeStart(StringUtils.leftPad(StringUtils.right(id, 3), StringUtils.length(id), "*"),
"***"));
}
/**
* [固定电话] 后四位,其他隐藏<例子:****1234>
*/
public static String fixedPhone(final String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");
}
/**
* [手机号码] 前三位,后四位,其他隐藏<例子:138******1234>
*/
public static String mobilePhone(final String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.left(num, 2).concat(StringUtils
.removeStart(StringUtils.leftPad(StringUtils.right(num, 2), StringUtils.length(num), "*"),
"***"));
}
/**
* [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护<例子:北京市海淀区****>
*
* @param sensitiveSize 敏感信息长度
*/
public static String address(final String address, final int sensitiveSize) {
if (StringUtils.isBlank(address)) {
return "";
}
final int length = StringUtils.length(address);
return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
}
/**
* [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示<例子:g**@163.com>
*/
public static String email(final String email) {
if (StringUtils.isBlank(email)) {
return "";
}
final int index = StringUtils.indexOf(email, "@");
if (index <= 1) {
return email;
} else {
return StringUtils.rightPad(StringUtils.left(email, 1), index, "*")
.concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
}
/**
* [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>
*/
public static String bankCard(final String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(
StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"),
"******"));
}
/**
* [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号<例子:12********>
*/
public static String cnapsCode(final String code) {
if (StringUtils.isBlank(code)) {
return "";
}
return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*");
}
}
对应VO实体类加上注解
这一开始是想从数据库出库时就加密,单考虑到逻辑处理代码中可能会用到手机号做关联或者查询,最后只在视图对象VO上加了注解,只有返回给界面时才会加密,这就需要吧VO(视图对象)、PO(持久化对象)、DTO(数据传输对象)拆分开
@Data
public class StoreInfoResponse implements Serializable {
private static final long serialVersionUID=1L;
//默认type是手机号加密,所以这省略了,没写
@SensitiveInfo(permission="sensitive_addressPhone_view")
private String addressPhone;
}
验证方式
main函数
因为这里使用的是Jackson 序列化,所以只有使用Jackson序列化时才能生效,使用fastjson序列化是不会生效的,这里我们使用手动序列化进行测试,实际项目中可以用页面请求的方式测试权限相关逻辑。
public static void main(String[] args) throws JsonProcessingException {
StoreInfoResponse response = new StoreInfoResponse ();
response.setAddressPhone("18231148754");
String jsonstr = new ObjectMapper().writeValueAsString(response);
System.out.println("jackson序列化>>>" + jsonstr);
System.out.println("fastjson序列化>>>" + JSON.toJSONString(response));
}
运行调试
运行结果:
jackson序列化>>>{"addressPhone":"18******54"}
fastjson序列化>>>{"addressPhone":"18231148754"}
进程已结束,退出代码为 0
关注公众号,每天进步一点点!