金融使用者敏感資料如何優雅地實現脫敏?

2023-06-01 06:01:00

專案介紹

紀錄檔脫敏是常見的安全需求。普通的基於工具類方法的方式,對程式碼的入侵性太強,編寫起來又特別麻煩。

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("[email protected]");
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='[email protected]', 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='[email protected]', 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='[email protected]', phone='18888888888'}], userSet=[User{username='脫敏君', idCard='123456190001011234', password='1234567', email='[email protected]', phone='18888888888'}], userCollection=[User{username='脫敏君', idCard='123456190001011234', password='1234567', email='[email protected]', phone='18888888888'}], userMap={map=User{username='脫敏君', idCard='123456190001011234', password='1234567', email='[email protected]', phone='18888888888'}}}";
final String commonJson = "{\"userArray\":[{\"email\":\"[email protected]\",\"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