使用十一种设计模式实现 基于Jackson注解的 完美数据脱敏方案


前言

又是好长时间没有写分享了,对我来说,写博客消耗的心力太大了,只能偶尔心血来潮了,没法收回曾经立下的Flag了。


一、背景

最近有数据脱敏的需求,我三下五除二就解决了,就是太丑了,直接耦合在业务处理中。所以我一直在寻找更加优雅的方案,例如 Mybatis 插件了,工具类等等,但都感觉差点意思。终于 当我看到了这篇博文:自定义注解实现数据序列化时进行数据脱敏,这就是我想要的,使用 Jackson 注解。但当我看完后马上意识到,他的方式还不够灵活。
我 立刻就有了改造的灵感,下文就是 使用 外观 + 策略 + 命令 + 工厂 + 静态代理 + 装饰器 + 解释器 + 适配器 + 享元 + 桥接 + 模板(有点牵强了,就是为了硬凑贯口) 改造后的成品,提供更强的灵活性。

通过这个样例,可以学习三个事情

  1. 学习Jackson 这种提供扩展的方式,太方便了
  2. 设计模式只是参考,六大原则才是核心目标,没有必要太教条。
  3. lambda 相关技术太方便了, 尤其对设计模式的实现,如果可以尽量多用。

二、代码实现

2.1 对脱敏操作抽象出函数式接口

这个接口 是 策略 + 命令 结合后的抽象,并通过 default 函数实现装饰功能。使用注解Sensitive 实例来实现了命令模式 ,可以更灵活的控制脱敏执行。

@FunctionalInterface
public interface SensitiveDeal {
	String apply(String source, Sensitive sensitive);

	default SensitiveDeal compose(SensitiveDeal before) {
		Objects.requireNonNull(before);
		return (source, sensitive) -> apply(before.apply(source, sensitive), sensitive);
	}

	default SensitiveDeal andThen(SensitiveDeal after) {
		Objects.requireNonNull(after);
		return (source, sensitive) -> after.apply(apply(source, sensitive), sensitive);
	}
}

2.2 Jackson自定义注解

@Documented 表示该注解会生成到 javaDoc中
@Retention(RetentionPolicy.RUNTIME) 表示该注解在运行时生效
@Target(ElementType.FIELD) 表示注解作用于字段上
@JacksonAnnotationsInside 这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) 指定使用自定义的序列化器
参数说明见源码注释。

Sensitive 可以使用相同的一套参数来配置脱敏实现,可以说 当前的脱敏方案 提供了 统一的外观。
Strategy.CUSTOMER 策略提供了适配的支持,使用Sensitive.dealType 就可以将第三方实现嵌入当前框架。
重点注意一下 Strategy 继承了 SensitiveDeal,实现了静态代理,体会这种写法的便利。
Strategy 的实例化使用了 简单工厂,策略 + 命令模式,稍微有点复杂,但是对外暴露的时候,就是普通的策略,使用者并不关心策略本身是如何被创建的。
Sensitive.regular 正则表达式 使用了 解释器模式,这个使用有点过于划水了。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {

	/**
	 * 脱敏策略,参考{@link Sensitive.Strategy}
	 */
	Strategy value();

	/**
	 * 有无参构造函数的{@link SensitiveDeal}实现类,用于完全自定义脱敏,参考
	 * {@link Sensitive.Strategy#CUSTOMER}
	 */
	Class<? extends SensitiveDeal> dealType() default SensitiveDeal.class;

	/**
	 * 正则表达式,参考具体策略的注释。
	 */
	String regular() default "";

	/**
	 * 替换后显示,与 {@link Sensitive#regular()}配合可以实现更方便的正则替换。<br>
	 * 剩余情况默认只取首位字符来填充。
	 */
	String replacement() default "*";

	/**
	 * 左侧需要保留几位明文字段,参考具体策略的注释。
	 */
	int maskStart() default 0;

	/**
	 * 右侧最多保留几位明文,参考具体策略的注释。
	 */
	int keepTail() default 0;

	/**
	 * 具体的脱敏策略实现
	 * 
	 * @author zkr-liuchunming
	 * @see SensitiveDeals
	 * @see SensitiveDeal
	 *
	 */
	enum Strategy implements SensitiveDeal {

		// 以下是 与业务字段相关的策略
		/**
		 * 性别
		 */
		SEX(SensitiveDeals::none),
		/**
		 * 用户名
		 */
		USERNAME(SensitiveDeals::userName),
		/**
		 * 密码
		 */
		PASSWORD(SensitiveDeals::none),
		/**
		 * 证件号码
		 */
		IDNO(SensitiveDeals.createWrap(6, 4)),
		/**
		 * 座机号
		 */
		FIXED_PHONE(SensitiveDeals.createSuffix(4)),
		/**
		 * 手机号
		 */
		MOBILE_PHONE(SensitiveDeals.createWrap(3, 4)),
		/**
		 * 邮箱
		 */
		EMAIL(SensitiveDeals::email),
		/**
		 * 银行账号
		 */
		BANK_ACCNO(SensitiveDeals.createWrap(6, 4)),
		/**
		 * 地址
		 */
		ADDRESS(SensitiveDeals.createRegular("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****")),

		// 以下是 与业务无关的通用策略,如果经常脱敏某种业务字段,建议抽成 业务字段策略,更有可读性
		/**
		 * 全部隐藏,例如密码之类
		 */
		NONE(SensitiveDeals::none),
		/**
		 * 只留结尾四个字符
		 */
		SUFFIX_4(SensitiveDeals.createSuffix(4)),
		/**
		 * 只保留结尾的通用脱敏,使用 {@link Sensitive#keepTail()}
		 */
		SUFFIX(SensitiveDeals::suffix),
		/**
		 * 只保留前缀的通用脱敏,使用 {@link Sensitive#maskStart()}
		 */
		PREFIX(SensitiveDeals::prefix),
		/**
		 * 首尾保留,中间至少有一个脱敏字符,使用 {@link Sensitive#maskStart()}
		 * {@link Sensitive#keepTail() }
		 */
		WRAP(SensitiveDeals::wrap),
		/**
		 * 使用正则表达式的通用脱敏,使用 {@link Sensitive.regular()}
		 * {@link Sensitive#replacement()}
		 */
		REGULAR(SensitiveDeals::regular),
		/**
		 * 使用 {@link Sensitive#dealType()},完全自定义脱敏实现,本策略最好只作应急。
		 */
		CUSTOMER(SensitiveDeals::customerCommon);

		final SensitiveDeal deal;

		private Strategy(SensitiveDeal deal) {
			Objects.requireNonNull(deal);
			this.deal = deal;
		}

		@Override
		public String apply(String source, Sensitive sensitive) {
			if (StringUtils.isBlank(source)) {
				return source;
			}
			return deal.apply(source, sensitive);
		}
	}
}

2.2 自定义脱敏序列化器

由于 Strategy 继承了 SensitiveDealSensitiveJsonSerializer 继承了 接口 ContextualSerializer ,因为三方依赖的是 接口(SensitiveDealJacksonContextualSerializer 的实现类),都可以独立地变化,这就是 桥接模式。同时 SensitiveJsonSerializer 也继承了JsonSerializerserialize何时被调用,是归 Jackson 管的,这可以说是 广义的 模板方法,虽然和标准的模板方法并不一样。

class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {

	Sensitive sensitive;

	SensitiveDeal deal;

	@Override
	public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
			throws JsonMappingException {
		Sensitive sensitive = property.getAnnotation(Sensitive.class);
		Objects.requireNonNull(sensitive, "只能通过 @Sensitive的方式使用,无法单独用");
		if (!Objects.equals(String.class, property.getType().getRawClass())) {
			throw new IllegalArgumentException("无法序列化成字符串的属性,不能脱敏。" + property.getMember());
		}
		this.sensitive = sensitive;
		this.deal = sensitive.value();
		return this;
	}

	@Override
	public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
			throws IOException, JsonProcessingException {
		gen.writeString(deal.apply(value, sensitive));
	}
}

2.3 提供脱敏的具体实现

@UtilityClass 是lombok 提供的注解,可以理解为,这个类里的所有的函数属性,都变成静态的了。
createSuffixcreatePrefixcreateWrapcreateRegular 使用工厂模式对外提供 SensitiveDeal 的实现。
regularCachecustomerCache 使用缓存技术,这个就是 享元模式。
虽然 SensitiveDeal 支持装饰模式,可以用更简洁的写法实现,但为了相对来说更好的性能,我 对每种做了单独的实现。

@UtilityClass
class SensitiveDeals {

	String none(String source, Sensitive sensitive) {
		char hidedChar = sensitive.replacement().charAt(0);
		char[] chars = new char[source.length()];
		Arrays.fill(chars, hidedChar);
		return new String(chars);
	}

	SensitiveDeal createSuffix(int showLength) {
		return (source, sensitive) -> {
			char hidedChar = sensitive.replacement().charAt(0);
			return suffix0(source, showLength, hidedChar);
		};
	}

	String suffix(String source, Sensitive sensitive) {
		char hidedChar = sensitive.replacement().charAt(0);
		return suffix0(source, sensitive.keepTail(), hidedChar);
	}

	SensitiveDeal createPrefix(int showLength) {
		return (source, sensitive) -> {
			char hidedChar = sensitive.replacement().charAt(0);
			return prefix0(source, showLength, hidedChar);
		};
	}

	String prefix(String source, Sensitive sensitive) {
		char hidedChar = sensitive.replacement().charAt(0);
		return prefix0(source, sensitive.maskStart(), hidedChar);
	}

	SensitiveDeal createWrap(final int maskStart, final int keepTail) {
		return (source, sensitive) -> {
			char hidedChar = sensitive.replacement().charAt(0);
			return wrap0(source, maskStart, keepTail, hidedChar);
		};
	}

	String wrap(String source, Sensitive sensitive) {
		char hidedChar = sensitive.replacement().charAt(0);
		return wrap0(source, sensitive.maskStart(), sensitive.keepTail(), hidedChar);
	}

	/**
	 * Pattern 是线程安全的,所以缓存起来。<br>
	 * 实例数量不会有太多,暂时不需要使用复杂缓存策略。
	 */
	Map<String, Pattern> regularCache = new ConcurrentHashMap<>();

	SensitiveDeal createRegular(String regular, String replacement) {
		Assert.notBlank(regular);
		Pattern pattern = regularCache.computeIfAbsent(regular, Pattern::compile);
		return (source, sensitive) -> {
			return regular0(source, pattern, replacement);
		};
	}

	String regular(String source, Sensitive sensitive) {
		Assert.notBlank(sensitive.regular());
		Assert.notBlank(sensitive.replacement());
		Pattern pattern = regularCache.computeIfAbsent(sensitive.regular(), Pattern::compile);
		return regular0(source, pattern, sensitive.replacement());
	}

	private String suffix0(final String source, final int showLength, final char hidedChar) {
		if (StringUtils.length(source) <= showLength) {
			return source;
		}

		char[] chars = new char[source.length()];
		Arrays.fill(chars, hidedChar);
		int begin = source.length() - showLength;
		source.getChars(begin, source.length(), chars, begin);
		return new String(chars);
	}

	private String prefix0(final String source, final int showLength, final char hidedChar) {
		if (StringUtils.length(source) <= showLength) {
			return source;
		}

		char[] chars = new char[source.length()];
		Arrays.fill(chars, hidedChar);
		source.getChars(0, showLength, chars, 0);
		return new String(chars);
	}

	private String wrap0(final String source, final int maskStart, final int keepTail, final char hidedChar) {
		if (source.length() <= maskStart + keepTail + 1) {
			char[] chars = source.toCharArray();
			chars[source.length() / 2] = hidedChar;
			return new String(chars);
		}

		char[] chars = new char[source.length()];
		Arrays.fill(chars, hidedChar);
		source.getChars(0, maskStart, chars, 0);
		int begin = source.length() - keepTail;
		source.getChars(begin, source.length(), chars, begin);
		return new String(chars);
	}

	private String regular0(final String source, final Pattern pattern, final String replacement) {
		Matcher matcher = pattern.matcher(source);
		if (!matcher.matches()) {
			return source;
		}
		return matcher.replaceAll(replacement);
	}

	/**
	 * 自定义脱敏的实例。肯定线程安全。。<br>
	 * 实例数量不会有太多,暂时不需要使用复杂缓存策略。
	 */
	Map<Class<? extends SensitiveDeal>, SensitiveDeal> customerCache = new ConcurrentHashMap<>();

	String customerCommon(String source, Sensitive sensitive) {
		Class<? extends SensitiveDeal> clz = sensitive.dealType();
		Objects.requireNonNull(clz);
		SensitiveDeal deal = customerCache.computeIfAbsent(clz, cls -> {
			try {
				return cls.newInstance();
			} catch (InstantiationException | IllegalAccessException e) {
				throw new IllegalArgumentException("脱敏策略无法实例化:" + cls.getName(), e);
			}
		});
		return deal.apply(source, sensitive);
	}

	String userName(String source, Sensitive sensitive) {
		if (StringUtils.length(source) < 2) {
			return source;
		}
		char hidedChar = sensitive.replacement().charAt(0);
		if (source.length() < 5) {
			char[] chars = source.toCharArray();
			chars[1] = hidedChar;
			return new String(chars);
		}

		char[] chars = new char[source.length()];
		Arrays.fill(chars, hidedChar);
		source.getChars(0, 1, chars, 0);
		int begin = source.length() - 2;
		source.getChars(begin, source.length(), chars, begin);
		return new String(chars);
	}

	String email(String source, Sensitive sensitive) {
		int index = StringUtils.indexOf(source, "@");
		if (index <= 0) {
			return source;
		}
		char hidedChar = sensitive.replacement().charAt(0);
		String acc = source.substring(0, index);
		String host = source.substring(index);
		return wrap0(acc, 2, 1, hidedChar) + host;
	}
}


三、使用实例

public class Person {
   @Sensitive(Strategy.USERNAME)
   String name;
   @Sensitive(Strategy.MOBILE_PHONE)
   String mobile;
   @Sensitive(Strategy.IDNO)
   String idNo;
   @Sensitive(Strategy.PASSWORD)
   String passWord;
   @Sensitive(value = Strategy.SUFFIX, keepTail = 3)
   String suffix3;
   @Sensitive(value = Strategy.CUSTOMER, dealType = MyDeal.class)
   String customeFile;
 }
 class MyDeal implements SensitiveDeal {
   public String apply(String source, Sensitive sensitive) {
     return "我是自定义脱敏";
   }
 }
 
 Person person = new Person("大马哈", "18888426666", "222369198305162015", "Aa778778", "我只会保留最后几个", "我会被代替");
 objectMapper.writeValueAsString(person);
{
  "name": "大*哈",
  "mobile": "188****6666",
  "idNo": "2223**********2015",
  "passWord": "********",
  "suffix3": "******后几个",
  "customeFile": "我是自定义脱敏"
}

总结

希望读者看完,了解三个事情:

  1. 数据脱敏本身的实现。
  2. 设计模式的灵活使用。
  3. lambda 对设计模式实现的便利。
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值