关于安全:金融用户敏感数据如何优雅地实现脱敏

10次阅读

共计 12135 个字符,预计需要花费 31 分钟才能阅读完成。

我的项目介绍

日志脱敏是常见的平安需要。一般的基于工具类办法的形式,对代码的入侵性太强,编写起来又特地麻烦。

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

日志脱敏

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

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

长久化加密

存储的时候下面的信息都须要加密,明码为不可逆加密,其余为可逆加密。

相似的性能有很多。不在本零碎的解决范畴内。

个性

  1. 基于注解的日志脱敏。
  2. 能够自定义策略实现,策略失效条件。
  3. 内置常见的十几种脱敏内置计划。
  4. java 深拷贝,且原始对象不必实现任何接口。
  5. 反对用户自定义注解。
  6. 反对基于 FastJSON 间接生成脱敏后的 json

变更日志

变更日志

疾速开始

环境筹备

JDK 7+

Maven 3.x

maven 导入

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

外围 api 简介

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 打印日志信息。

@Sensitive 注解

阐明

@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) 身份证脱敏
6 @SensitiveStrategyAddress @Sensitive(strategy = StrategyAddress.class) 地址脱敏
7 @SensitiveStrategyGps @Sensitive(strategy = StrategyGps.class) GPS 脱敏
8 @SensitiveStrategyIp @Sensitive(strategy = StrategyIp.class) IP 脱敏
9 @SensitiveStrategyBirthday @Sensitive(strategy = StrategyBirthday.class) 生日脱敏
10 @SensitiveStrategyPassport @Sensitive(strategy = StrategyPassport.class) 护照脱敏
11 @SensitiveStrategyMaskAll @Sensitive(strategy = StrategyMaskAll.class) 全副脱敏
12 @SensitiveStrategyMaskHalf @Sensitive(strategy = StrategyMaskHalf.class) 一半脱敏
13 @SensitiveStrategyMaskRange @Sensitive(strategy = StrategyMaskRange.class) 指定范畴脱敏

@Sensitive 定义

@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {

    /**
     * 注解失效的条件
     * @return 条件对应的实现类
     */
    Class<? extends ICondition> condition() default ConditionAlwaysTrue.class;

    /**
     * 执行的策略
     * @return 策略对应的类型
     */
    Class<? extends IStrategy> strategy();}

与 @Sensitive 混合应用

如果你将新增的注解 @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 新增性能。容许性能自定义级联脱敏注解。

案例 1

自定义明码脱敏策略 & 自定义明码脱敏策略失效条件

  • 策略脱敏
/**
 * 自定义明码脱敏策略
 * @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;
}

案例 2

  • 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;

    // 其余办法...
}

生成脱敏后的 JSON

阐明

为了防止生成两头脱敏对象,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。

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)

脱敏疏导类

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

外围 api 简介

SensitiveBs 疏导类的外围办法列表如下:

序号 办法 参数 后果 阐明
1 desCopy() 指标对象 深度拷贝脱敏对象 适应性更强
2 desJson() 指标对象 脱敏对象 json 性能较好

应用示例

应用形式和工具类统一,示意如下:

SensitiveBs.newInstance().desCopy(user);

配置深度拷贝实现

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

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

参见 SensitiveBsTest.java

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

深度复制(DeepCopy)

阐明

深度复制能够保障咱们日志输入对象脱敏,同时不影响失常业务代码的应用。

能够实现深度复制的形式有很多种,默认基于 fastjson 实现的。

为保障后续良性倒退,v0.0.13 版本之后将深度复制接口抽离为独自的我的项目:

deep-copy

内置策略

目前反对 6 种基于序列化实现的深度复制,便于用户替换应用。

每一种都能够独自应用,保障依赖更加轻量。

自定义

为满足不同场景的需要,深度复制策略反对用户自定义。

自定义深度复制

开源地址

https://github.com/houbb/sensitive

正文完
 0