前言
又是好长时间没有写分享了,对我来说,写博客消耗的心力太大了,只能偶尔心血来潮了,没法收回曾经立下的Flag了。
一、背景
最近有数据脱敏的需求,我三下五除二就解决了,就是太丑了,直接耦合在业务处理中。所以我一直在寻找更加优雅的方案,例如 Mybatis 插件了,工具类等等,但都感觉差点意思。终于 当我看到了这篇博文:自定义注解实现数据序列化时进行数据脱敏,这就是我想要的,使用 Jackson 注解。但当我看完后马上意识到,他的方式还不够灵活。
我 立刻就有了改造的灵感,下文就是 使用 外观 + 策略 + 命令 + 工厂 + 静态代理 + 装饰器 + 解释器 + 适配器 + 享元 + 桥接 + 模板(有点牵强了,就是为了硬凑贯口) 改造后的成品,提供更强的灵活性。
通过这个样例,可以学习三个事情
- 学习Jackson 这种提供扩展的方式,太方便了
- 设计模式只是参考,六大原则才是核心目标,没有必要太教条。
- 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
继承了 SensitiveDeal
,SensitiveJsonSerializer
继承了 接口 ContextualSerializer
,因为三方依赖的是 接口(SensitiveDeal
、 Jackson 和 ContextualSerializer
的实现类),都可以独立地变化,这就是 桥接模式。同时 SensitiveJsonSerializer
也继承了JsonSerializer
,serialize
何时被调用,是归 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 提供的注解,可以理解为,这个类里的所有的函数属性,都变成静态的了。
createSuffix
、createPrefix
、createWrap
、createRegular
使用工厂模式对外提供 SensitiveDeal
的实现。
regularCache
、customerCache
使用缓存技术,这个就是 享元模式。
虽然 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": "我是自定义脱敏"
}
总结
希望读者看完,了解三个事情:
- 数据脱敏本身的实现。
- 设计模式的灵活使用。
lambda
对设计模式实现的便利。