Java注解实现数据脱敏-与主业务解耦且权限管控关联


最近业务需求,为了防止用户手机号等敏感信息泄漏,需要把手机号进行密文处理,考虑到以下两个问题

  1. 业务多逻辑复杂,挨个找代码改,太费劲,而且无法保证能全部找到并改完
  2. 不同用户需求不通,部分用户需要查看手机号,部分用户不需要查看手机号
  3. 脱敏跟主业务相关度低,不想代码侵入太严重
    想到这个逻辑跟转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

关注公众号,每天进步一点点!
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huihttp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值