紀錄檔脫敏是常見的安全需求。普通的基於工具類方法的方式,對程式碼的入侵性太強,編寫起來又特別麻煩。
sensitive 提供了基於註解的方式,並且內建了常見的脫敏方式,便於開發。
為了金融交易的安全性,國家強制規定對於以下資訊是要紀錄檔脫敏的:
使用者名稱
手機號
郵箱
銀行卡號
密碼
身份證號
儲存的時候上面的資訊都需要加密,密碼為不可逆加密,其他為可逆加密。
類似的功能有很多。不在本系統的解決範圍內。
基於註解的紀錄檔脫敏。
可以自定義策略實現,策略生效條件。
內建常見的十幾種脫敏內建方案。
java 深拷貝,且原始物件不用實現任何介面。
支援使用者自定義註解。
支援基於 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 集合 |
通過註解,指定每一個欄位的脫敏策略。
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
列印紀錄檔資訊。
@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) |
指定範圍脫敏 |
@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,你覺得這種使用者不脫敏也罷。
@Sensitive(condition = ConditionFooPassword.class, strategy = StrategyPassword.class)
private String password;
其他保持不變,我們指定了一個 condition,實現如下:
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
。
會遍歷每一個屬性,執行上面的脫敏策略。
會處理物件中各個欄位上的脫敏註解資訊。
遍歷每一個物件,處理物件中各個欄位上的脫敏註解資訊。
作為演示,集合中為普通的字串。
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;
//...
}
/**
* 自定義密碼脫敏策略
* @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{
}
@SensitiveStrategy
策略單獨使用的時候,預設是生效的。
如果有 @SensitiveCondition
註解,則只有當條件滿足時,才會執行脫敏策略。
@SensitiveCondition
只會對系統內建註解和自定義註解生效,因為 @Sensitive
有屬於自己的策略生效條件。
@Sensitive
優先生效,然後是系統內建註解,最後是使用者自定義註解。
兩個元註解 @SensitiveStrategy
、@SensitiveCondition
分別指定了對應的實現。
public class CustomPasswordStrategy implements IStrategy {
@Override
public Object des(Object original, IContext context) {
return "**********************";
}
}
/**
* 讓這些 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;
}
可以根據自己的業務需要,在自定義的註解上使用 @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='[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)
。
為了設定的靈活性,引入了引導類。
SensitiveBs
引導類的核心方法列表如下:
序號 | 方法 | 引數 | 結果 | 說明 |
---|---|---|---|---|
1 | desCopy() | 目標物件 | 深度拷貝脫敏物件 | 適應性更強 |
2 | desJson() | 目標物件 | 脫敏物件 json | 效能較好 |
使用方式和工具類一致,示意如下:
SensitiveBs.newInstance().desCopy(user);
預設的使用 FastJson 進行物件的深度拷貝,等價於:
SensitiveBs.newInstance()
.deepCopy(FastJsonDeepCopy.getInstance())
.desJson(user);
deepCopy 用於指定深度複製的具體實現,支援使用者自定義。
深度複製可以保證我們紀錄檔輸出物件脫敏,同時不影響正常業務程式碼的使用。
可以實現深度複製的方式有很多種,預設基於 fastjson 實現的。
為保證後續良性發展,v0.0.13 版本之後將深度複製介面抽離為單獨的專案:
目前支援 6 種基於序列化實現的深度複製,便於使用者替換使用。
每一種都可以單獨使用,保證依賴更加輕量。
為滿足不同場景的需求,深度複製策略支援使用者自定義。