金融用户敏感数据如何优雅地实现脱敏?

文章目录

金融用户敏感数据如何优雅地实现脱敏?

日志脱敏是常见的安全需求。普通的基于工具类方法的方式,对代码的入侵性太强,编写起来又特别麻烦。

sensitive[1] 提供了基于注解的方式,并且内置了常见的脱敏方式,便于开发。

为了金融交易的安全性,国家强制规定对于以下信息是要日志脱敏的:

  1. 用户名
  2. 手机号
  3. 邮箱
  4. 银行卡号
  5. 密码
  6. 身份证号

  1. 基于注解的日志脱敏。
  2. 可以自定义策略实现,策略生效条件。
  3. 内置常见的十几种脱敏内置方案。
  4. java 深拷贝,且原始对象不用实现任何接口。
  5. 支持用户自定义注解。
  6. 支持基于 FastJSON 直接生成脱敏后的 json。

  • JDK 7+
  • Maven 3.x

<dependency>
 <groupId>com.github.houbb</groupId>
 <artifactId>sensitive-core</artifactId>
 <version>1.0.0</version>
</dependency>

SensitiveUtil 工具类的核心方法列表如下:

序号

方法

参数

结果

说明

1

desCopy()

目标对象

深度拷贝脱敏对象

适应性更强

2

desJson()

目标对象

脱敏对象 json

性能较好

3

desCopyCollection()

目标对象集合

深度拷贝脱敏对象集合

4

desJsonCollection()

目标对象集合

脱敏对象 json 集合

  • UserAnnotationBean.java

通过注解,指定每一个字段的脱敏策略。

public class UserAnnotationBean {
 @SensitiveStrategyChineseName
 private String username;
 @SensitiveStrategyPassword
 private String password;
 @SensitiveStrategyPassport
 private String passport;
 @SensitiveStrategyIdNo
 private String idNo;
 @SensitiveStrategyCardId
 private String bandCardId;
 @SensitiveStrategyPhone
 private String phone;
 @SensitiveStrategyEmail
 private String email;
 @SensitiveStrategyAddress
 private String address;
 @SensitiveStrategyBirthday
 private String birthday;
 @SensitiveStrategyGps
 private String gps;
 @SensitiveStrategyIp
 private String ip;
 @SensitiveStrategyMaskAll
 private String maskAll;
 @SensitiveStrategyMaskHalf
 private String maskHalf;
 @SensitiveStrategyMaskRange
 private String maskRange;
 //Getter & Setter
 //toString()
}
  • 数据准备

构建一个最简单的测试对象:

UserAnnotationBean bean = new UserAnnotationBean();
bean.setUsername("张三");
bean.setPassword("123456");
bean.setPassport("CN1234567");
bean.setPhone("13066668888");
bean.setAddress("中国上海市浦东新区外滩18号");
bean.setEmail("whatanice@code.com");
bean.setBirthday("20220831");
bean.setGps("66.888888");
bean.setIp("127.0.0.1");
bean.setMaskAll("可恶啊我会被全部掩盖");
bean.setMaskHalf("还好我只会被掩盖一半");
bean.setMaskRange("我比较灵活指定掩盖范围");
bean.setBandCardId("666123456789066");
bean.setIdNo("360123202306018888");
  • 测试代码
final String originalStr = "UserAnnotationBean{username='张三', password='123456', passport='CN1234567', idNo='360123202306018888', bandCardId='666123456789066', phone='13066668888', email='whatanice@code.com', address='中国上海市浦东新区外滩18号', birthday='20220831', gps='66.888888', ip='127.0.0.1', maskAll='可恶啊我会被全部掩盖', maskHalf='还好我只会被掩盖一半', maskRange='我比较灵活指定掩盖范围'}";
final String sensitiveStr = "UserAnnotationBean{username='张*', password='null', passport='CN*****67', idNo='3****************8', bandCardId='666123*******66', phone='1306****888', email='wh************.com', address='中国上海********8号', birthday='20*****1', gps='66*****88', ip='127***0.1', maskAll='**********', maskHalf='还好我只会*****', maskRange='我*********围'}";
final String expectSensitiveJson = "{\"address\":\"中国上海********8号\",\"bandCardId\":\"666123*******66\",\"birthday\":\"20*****1\",\"email\":\"wh************.com\",\"gps\":\"66*****88\",\"idNo\":\"3****************8\",\"ip\":\"127***0.1\",\"maskAll\":\"**********\",\"maskHalf\":\"还好我只会*****\",\"maskRange\":\"我*********围\",\"passport\":\"CN*****67\",\"phone\":\"1306****888\",\"username\":\"张*\"}";

UserAnnotationBean sensitiveUser = SensitiveUtil.desCopy(bean);
Assert.assertEquals(sensitiveStr, sensitiveUser.toString());
Assert.assertEquals(originalStr, bean.toString());
String sensitiveJson = SensitiveUtil.desJson(bean);
Assert.assertEquals(expectSensitiveJson, sensitiveJson);

我们可以直接利用 sensitiveUser 去打印日志信息,而这个对象对于代码其他流程不影响,我们依然可以使用原来的 user 对象。

当然,也可以使用 sensitiveJson 打印日志信息。

@SensitiveStrategyChineseName 这种注解是为了便于用户使用,本质上等价于 @Sensitive(strategy = StrategyChineseName.class)

@Sensitive 注解可以指定对应的脱敏策略。

编号

注解

等价 @Sensitive

备注

1

@SensitiveStrategyChineseName

@Sensitive(strategy = StrategyChineseName.class)

中文名称脱敏

2

@SensitiveStrategyPassword

@Sensitive(strategy = StrategyPassword.class)

密码脱敏

3

@SensitiveStrategyEmail

@Sensitive(strategy = StrategyEmail.class)

email 脱敏

4

@SensitiveStrategyCardId

@Sensitive(strategy = StrategyCardId.class)

卡号脱敏

5

@SensitiveStrategyPhone

@Sensitive(strategy = StrategyPhone.class)

手机号脱敏

6

@SensitiveStrategyIdNo

@Sensitive(strategy = StrategyIdNo.class)

身份证脱敏

7

@SensitiveStrategyAddress

@Sensitive(strategy = StrategyAddress.class)

地址脱敏

8

@SensitiveStrategyGps

@Sensitive(strategy = StrategyGps.class)

GPS 脱敏

9

@SensitiveStrategyIp

@Sensitive(strategy = StrategyIp.class)

IP 脱敏

10

@SensitiveStrategyBirthday

@Sensitive(strategy = StrategyBirthday.class)

生日脱敏

11

@SensitiveStrategyPassport

@Sensitive(strategy = StrategyPassport.class)

护照脱敏

12

@SensitiveStrategyMaskAll

@Sensitive(strategy = StrategyMaskAll.class)

全部脱敏

13

@SensitiveStrategyMaskHalf

@Sensitive(strategy = StrategyMaskHalf.class)

一半脱敏

14

@SensitiveStrategyMaskRange

@Sensitive(strategy = StrategyMaskRange.class)

指定范围脱敏

@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
 /**
 * 注解生效的条件
 * @return 条件对应的实现类
 */
 Class<? extends ICondition> condition() default ConditionAlwaysTrue.class;
 /**
 * 执行的策略
 * @return 策略对应的类型
 */
 Class<? extends IStrategy> strategy();


}

如果你将新增的注解 @SensitiveStrategyChineseName 与 @Sensitive 同时在一个字段上使用。

为了简化逻辑,优先选择执行 @Sensitive,如果 @Sensitive 执行脱敏, 那么 @SensitiveStrategyChineseName 将不会生效。

如:

/**
 * 测试字段
 * 1.当多种注解混合的时候,为了简化逻辑,优先选择 @Sensitive 注解。
 */
@SensitiveStrategyChineseName
@Sensitive(strategy = StrategyPassword.class)
private String testField;

默认情况下,我们指定的场景都是生效的。

但是你可能需要有些情况下不进行脱敏,比如有些用户密码为 123456,你觉得这种用户不脱敏也罢。

  • UserPasswordCondition.java
@Sensitive(condition = ConditionFooPassword.class, strategy = StrategyPassword.class)
private String password;

其他保持不变,我们指定了一个 condition,实现如下:

  • ConditionFooPassword.java
public class ConditionFooPassword implements ICondition {
 @Override
 public boolean valid(IContext context) {
 try {
 Field field = context.getCurrentField();
 final Object currentObj = context.getCurrentObject();
 final String password = (String) field.get(currentObj);
 return !password.equals("123456");
 } catch (IllegalAccessException e) {
 throw new RuntimeException(e);
 }
 }
}

也就是只有当密码不是 123456 时密码脱敏策略才会生效。

如果某个属性是单个集合或者对象,则需要使用注解 @SensitiveEntry

  • 放在集合属性上,且属性为普通对象

会遍历每一个属性,执行上面的脱敏策略。

  • 放在对象属性上

会处理对象中各个字段上的脱敏注解信息。

  • 放在集合属性上,且属性为对象

遍历每一个对象,处理对象中各个字段上的脱敏注解信息。

  • UserEntryBaseType.java

作为演示,集合中为普通的字符串。

public class UserEntryBaseType {
 @SensitiveEntry
 @Sensitive(strategy = StrategyChineseName.class)
 private List<String> chineseNameList;
 @SensitiveEntry
 @Sensitive(strategy = StrategyChineseName.class)
 private String[] chineseNameArray;
 //Getter & Setter & toString()
}

例子如下:

public class UserEntryObject {
 @SensitiveEntry
 private User user;
 @SensitiveEntry
 private List<User> userList;
 @SensitiveEntry
 private User[] userArray;
 //...
}

  • v0.0.4 新增功能。允许功能自定义条件注解和策略注解。•v0.0.11 新增功能。允许功能自定义级联脱敏注解。

  • 策略脱敏
/**
 * 自定义密码脱敏策略
 * @author binbin.hou
 * date 2019/1/17
 * @since 0.0.4
 */
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveStrategy(CustomPasswordStrategy.class)
public @interface SensitiveCustomPasswordStrategy {
}
  • 脱敏生效条件
/**
 * 自定义密码脱敏策略生效条件
 * @author binbin.hou
 * date 2019/1/17
 * @since 0.0.4
 */
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveCondition(ConditionFooPassword.class)
public @interface SensitiveCustomPasswordCondition{
}
  • TIPS

@SensitiveStrategy 策略单独使用的时候,默认是生效的。

如果有 @SensitiveCondition 注解,则只有当条件满足时,才会执行脱敏策略。

@SensitiveCondition 只会对系统内置注解和自定义注解生效,因为 @Sensitive 有属于自己的策略生效条件。

  • 策略优先级

@Sensitive 优先生效,然后是系统内置注解,最后是用户自定义注解。

两个元注解 @SensitiveStrategy@SensitiveCondition 分别指定了对应的实现。

  • CustomPasswordStrategy.java
public class CustomPasswordStrategy implements IStrategy {
 @Override
 public Object des(Object original, IContext context) {
 return "**********************";
 }
}
  • ConditionFooPassword.java
/**
 * 让这些 123456 的密码不进行脱敏
 * @author binbin.hou
 * date 2019/1/2
 * @since 0.0.1
 */
public class ConditionFooPassword implements ICondition {
 @Override
 public boolean valid(IContext context) {
 try {
 Field field = context.getCurrentField();
 final Object currentObj = context.getCurrentObject();
 final String name = (String) field.get(currentObj);
 return !name.equals("123456");
 } catch (IllegalAccessException e) {
 throw new RuntimeException(e);
 }
 }
}

定义一个使用自定义注解的对象。

public class CustomPasswordModel {
 @SensitiveCustomPasswordCondition
 @SensitiveCustomPasswordStrategy
 private String password;
 @SensitiveCustomPasswordCondition
 @SensitiveStrategyPassword
 private String fooPassword;
 //其他方法
}

/**
 * 自定义注解测试
 */
@Test
public void customAnnotationTest() {
 final String originalStr = "CustomPasswordModel{password='hello', fooPassword='123456'}";
 final String sensitiveStr = "CustomPasswordModel{password='**********************', fooPassword='123456'}";
 CustomPasswordModel model = buildCustomPasswordModel();
 Assert.assertEquals(originalStr, model.toString());
 CustomPasswordModel sensitive = SensitiveUtil.desCopy(model);
 Assert.assertEquals(sensitiveStr, sensitive.toString());
 Assert.assertEquals(originalStr, model.toString());
}

构建对象的方法如下:

/**
 * 构建自定义密码对象
 * @return 对象
 */
private CustomPasswordModel buildCustomPasswordModel(){
 CustomPasswordModel model = new CustomPasswordModel();
 model.setPassword("hello");
 model.setFooPassword("123456");
 return model;
}

  • v0.0.11 新增功能。允许功能自定义级联脱敏注解。

  • 自定义级联脱敏注解

可以根据自己的业务需要,在自定义的注解上使用 @SensitiveEntry

使用方式保持和 @SensitiveEntry 一样即可。

/**
 * 级联脱敏注解,如果对象中属性为另外一个对象(集合),则可以使用这个注解指定。
 * <p>
 * 1. 如果属性为 Iterable 的子类集合,则当做列表处理,遍历其中的对象
 * 2. 如果是普通对象,则处理对象中的脱敏信息
 * 3. 如果是普通字段/MAP,则不做处理
 * @since 0.0.11
 */
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveEntry
public @interface SensitiveEntryCustom {
}

定义一个使用自定义注解的对象。

public class CustomUserEntryObject {
 @SensitiveEntryCustom
 private User user;
 @SensitiveEntryCustom
 private List<User> userList;
 @SensitiveEntryCustom
 private User[] userArray;
 // 其他方法...
}

为了避免生成中间脱敏对象,v0.0.6 之后直接支持生成脱敏后的 JSON。

新增工具类方法,可以直接返回脱敏后的 JSON。

生成的 JSON 是脱敏的,原对象属性值不受影响。

public static String desJson(Object object)

和 SensitiveUtil.desCopy() 完全一致。

所有的测试案例中,都添加了对应的 desJson(Object) 测试代码,可以参考。

此处只展示最基本的使用。

final String originalStr = "SystemBuiltInAt{phone='18888888888', password='1234567', name='脱敏君', email='12345@qq.com', cardId='123456190001011234'}";
final String sensitiveJson = "{\"cardId\":\"123456**********34\",\"email\":\"12******.com\",\"name\":\"脱**\",\"phone\":\"1888****888\"}";
SystemBuiltInAt systemBuiltInAt = DataPrepareTest.buildSystemBuiltInAt();
Assert.assertEquals(sensitiveJson, SensitiveUtil.desJson(systemBuiltInAt));
Assert.assertEquals(originalStr, systemBuiltInAt.toString());

本次 JSON 脱敏基于 FastJSON[2]

FastJSON 在序列化本身存在一定限制。当对象中有集合,集合中还是对象时,结果不尽如人意。

本测试案例可见测试代码。

final String originalStr = "UserCollection{userList=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userSet=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userCollection=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userMap={map=User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}}}";
final String commonJson = "{\"userArray\":[{\"email\":\"12345@qq.com\",\"idCard\":\"123456190001011234\",\"password\":\"1234567\",\"phone\":\"18888888888\",\"username\":\"脱敏君\"}],\"userCollection\":[{\"$ref\":\"$.userArray[0]\"}],\"userList\":[{\"$ref\":\"$.userArray[0]\"}],\"userMap\":{\"map\":{\"$ref\":\"$.userArray[0]\"}},\"userSet\":[{\"$ref\":\"$.userArray[0]\"}]}";
final String sensitiveJson = "{\"userArray\":[{\"email\":\"12******.com\",\"idCard\":\"123456**********34\",\"phone\":\"1888****888\",\"username\":\"脱**\"}],\"userCollection\":[{\"$ref\":\"$.userArray[0]\"}],\"userList\":[{\"$ref\":\"$.userArray[0]\"}],\"userMap\":{\"map\":{\"$ref\":\"$.userArray[0]\"}},\"userSet\":[{\"$ref\":\"$.userArray[0]\"}]}";
UserCollection userCollection = DataPrepareTest.buildUserCollection();
Assert.assertEquals(commonJson, JSON.toJSONString(userCollection));
Assert.assertEquals(sensitiveJson, SensitiveUtil.desJson(userCollection));
Assert.assertEquals(originalStr, userCollection.toString());

如果有这种需求,建议使用原来的 desCopy(Object)

为了配置的灵活性,引入了引导类。

SensitiveBs 引导类的核心方法列表如下:

序号

方法

参数

结果

说明

1

desCopy()

目标对象

深度拷贝脱敏对象

适应性更强

2

desJson()

目标对象

脱敏对象 json

性能较好

使用方式和工具类一致,示意如下:

SensitiveBs.newInstance().desCopy(user);

默认的使用 FastJson 进行对象的深度拷贝,等价于:

SensitiveBs.newInstance()
 .deepCopy(FastJsonDeepCopy.getInstance())
 .desJson(user);

参见 SensitiveBsTest.java[3]。

deepCopy 用于指定深度复制的具体实现,支持用户自定义。

深度复制可以保证我们日志输出对象脱敏,同时不影响正常业务代码的使用。

可以实现深度复制的方式有很多种,默认基于 fastjson[4] 实现的。

为保证后续良性发展,v0.0.13 版本之后将深度复制接口抽离为单独的项目:

deep-copy[5]

目前支持 6 种基于序列化实现的深度复制,便于用户替换使用。

每一种都可以单独使用,保证依赖更加轻量。

为满足不同场景的需求,深度复制策略支持用户自定义。

自定义深度复制[6]

https://github.com/houbb/sensitive [7]。

[1] sensitive: https://github.com/houbb/sensitive。

[2] FastJSON: https://github.com/alibaba/fastjson。

[3] SensitiveBsTest.java: https://github.com/houbb/sensitive/blob/master/sensitive-test/src/test/java/com/github/houbb/sensitive/test/bs/SensitiveBsTest.java。

[4] fastjson: https://github.com/alibaba/fastjson。

[5] deep-copy: https://github.com/houbb/deep-copy。

[6] 自定义深度复制: https://github.com/houbb/deep-copy#自定义。

[7] https://github.com/houbb/sensitive : https://github.com/houbb/sensitive。

© 版权声明

相关文章