企業(yè)級(jí)數(shù)據(jù)脫敏方案!
最近幾年經(jīng)常發(fā)生用戶數(shù)據(jù)泄漏的事件,給企業(yè)帶來(lái)危機(jī)。隨著用戶對(duì)個(gè)人隱私數(shù)據(jù)的重視和法律法規(guī)的完善,數(shù)據(jù)安全顯得愈發(fā)重要。一方面可以加強(qiáng)權(quán)限管理,減少能夠接觸數(shù)據(jù)的人員以及導(dǎo)出數(shù)據(jù)加強(qiáng)審批。另一方面,還需要從技術(shù)上對(duì)用戶隱私數(shù)據(jù)進(jìn)行脫敏處理,提高數(shù)據(jù)的安全性。
數(shù)據(jù)脫敏方法有很多種,大致可以按照以下進(jìn)行分類:
- 隱藏法: 只顯示敏感信息的部分內(nèi)容,其他部分進(jìn)行遮擋,比較常見使用星號(hào)替代。這種方式日常比較多見,比如手機(jī)號(hào),銀行卡號(hào)等只顯示后面和后面幾位,好處是雖然只是部分內(nèi)容顯示,但足夠提供有效信息,同時(shí)不會(huì)暴露完整數(shù)據(jù)。
- 混淆法: 對(duì)原有數(shù)據(jù)截?cái)唷⑻鎿Q、隱藏、數(shù)字進(jìn)行隨機(jī)移位,使得原有數(shù)據(jù)完全失真或者部分失真,混淆真假。
- 加密: 通過(guò)加密密鑰和算法對(duì)敏感數(shù)據(jù)進(jìn)行加密得到密文,密文可見但是完全沒有可讀意義,是脫敏最徹底的方法。其中對(duì)稱加密還能密鑰解密可以從密文恢復(fù)原始數(shù)據(jù)。比如密碼保存采用非對(duì)稱加密,手機(jī)號(hào)存儲(chǔ)時(shí)采用對(duì)稱加密。
用戶的敏感數(shù)據(jù)包含姓名、電話號(hào)碼、身份證、銀行卡號(hào)、電子郵件、家庭住址、登錄密碼等等。需要考慮數(shù)據(jù)的敏感程度、數(shù)據(jù)安全要求以及實(shí)際業(yè)務(wù)使用場(chǎng)景選擇合適的脫敏方法。Hutool包里面提供了許多常用的脫敏方法。
企業(yè)脫敏方案
企業(yè)如何實(shí)現(xiàn)脫敏?我們先來(lái)看典型的系統(tǒng)數(shù)據(jù)交互鏈路,數(shù)據(jù)需要經(jīng)過(guò)數(shù)據(jù)庫(kù)、后端應(yīng)用、app端。
圖片
- 數(shù)據(jù)庫(kù)側(cè): 數(shù)據(jù)庫(kù)保存了原始數(shù)據(jù),有權(quán)限人員可以查看數(shù)據(jù)和導(dǎo)出數(shù)據(jù)。
- 后端應(yīng)用內(nèi): 后端應(yīng)用中會(huì)打印相關(guān)日志,數(shù)據(jù)通過(guò)日志得到了存儲(chǔ)下來(lái)。通過(guò)日志,能夠得到原始數(shù)據(jù)。
- 應(yīng)用輸出: app側(cè)能夠從后端讀取到原始數(shù)據(jù)。
數(shù)據(jù)庫(kù)脫敏方案
數(shù)據(jù)庫(kù)脫敏方法根據(jù)業(yè)務(wù)具體要求選擇合適脫敏方法。脫敏地點(diǎn)可以在應(yīng)用中手動(dòng)脫敏,當(dāng)然這種方法不常用,改動(dòng)點(diǎn)多對(duì)業(yè)務(wù)侵入大。
另外一種方案在ORM框架中修改sql實(shí)現(xiàn),其中mybatis框架為java后端系統(tǒng)中最常用的框架。mybatis自帶攔截器擴(kuò)展,允許在映射語(yǔ)句執(zhí)行過(guò)程中的某一點(diǎn)進(jìn)行攔截調(diào)用。關(guān)注公眾號(hào):碼猿技術(shù)專欄,回復(fù)關(guān)鍵詞:1111 獲取阿里內(nèi)部Java性能調(diào)優(yōu)手冊(cè)!默認(rèn)情況下,MyBatis 允許使用插件來(lái)攔截的方法調(diào)用包括:
- Executor: 攔截執(zhí)行器的方法,例如 update、query、commit、rollback 等。可以用來(lái)實(shí)現(xiàn)緩存、事務(wù)、分頁(yè)等功能。
- ParameterHandler: 攔截參數(shù)處理器的方法,例如 setParameters 等。可以用來(lái)轉(zhuǎn)換或加密參數(shù)等功能。
- ResultSetHandler: 攔截結(jié)果集處理器的方法,例如 handleResultSets、handleOutputParameters 等。可以用來(lái)轉(zhuǎn)換或過(guò)濾結(jié)果集等功能。
- StatementHandler: 攔截語(yǔ)句處理器的方法,例如 prepare、parameterize、batch、update、query 等。可以用來(lái)修改 SQL 語(yǔ)句、添加參數(shù)、記錄日志等功能。
Mybatis執(zhí)行流程
數(shù)據(jù)庫(kù)脫敏另外一個(gè)問(wèn)題是歷史數(shù)據(jù)問(wèn)題。歷史原因最開始的技術(shù)方案保存明文,所以脫敏時(shí)需要做到平滑脫敏。要做到平滑脫敏,可按照如下流程:
- 新增脫敏字段: 在源表上新增脫敏字段。
- 數(shù)據(jù)雙寫: 源字段和脫敏字段都寫入數(shù)據(jù)。
- 歷史數(shù)據(jù)遷移: 歷史數(shù)據(jù)遷移,刷入脫敏字段。
- 讀切換脫敏字段: 從脫敏字段讀取數(shù)據(jù)返回。
- 清空源字段: 確保所有流程都正確的情況下,清空源字段。
本文mybatis實(shí)現(xiàn)數(shù)據(jù)庫(kù)加解密為例。
1.表里面新增脫敏字段,示例中脫敏新字段格式規(guī)范為源字段添加encrypt后綴。vo里面添加脫敏注解標(biāo)記。
public class Employee {
private Long id;
private String name;
@EncryptTag
private String mobile;
private String mobileEncrypt;
private String email;
private double salary;
}
2.實(shí)現(xiàn)自定義攔截。
/***
** 加密攔截
***/
@Intercepts({@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class EncryptPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
Object param = invocation.getArgs()[1];
PluginService.encrypt(invocation, param);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
/***
** 解密攔截
***/
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)})
public class DecryptPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result != null && result instanceof List) {
this.decrypt(((List) result).iterator());
}
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
private void decrypt(Iterator iterator) throws Throwable {
while(iterator.hasNext()) {
Object object = iterator.next();
PluginService.decrypt(object);
}
}
}
3.實(shí)現(xiàn)sql修改,完成加解密邏輯。
public class PluginService {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(PluginService.class);
privatestaticfinal Map<String, List<Field>> ENCRYPT_TAG_FIELDS = new ConcurrentHashMap();
public static void encrypt(Invocation invocation, Object object) throws Throwable {
if (object.getClass().isArray()) {
int length = Array.getLength(object);
if (length <= 0) {
return;
}
for (int i = 0; i < length; ++i) {
encryptSingleObject(Array.get(object, i));
}
} elseif (object instanceof Collection) {
Collection collection = (Collection) object;
Iterator itr = collection.iterator();
while (itr.hasNext()) {
Object item = itr.next();
encryptSingleObject(item);
}
} else {
encryptSingleObject(object);
}
}
private static void encryptSingleObject(Object object) throws Throwable {
if (object != null) {
String className = object.getClass().getName();
List<Field> EncryptTagFields = ENCRYPT_TAG_FIELDS.get(className);
if (EncryptTagFields == null) {
EncryptTagFields = findEncryptTagFields(object);
ENCRYPT_TAG_FIELDS.putIfAbsent(className, EncryptTagFields);
}
encryptFields(object, EncryptTagFields);
}
}
private static void encryptFields(Object object, List<Field> EncryptTagFields) throws Throwable {
if (object != null && !EncryptTagFields.isEmpty()) {
String[] originalValues = new String[EncryptTagFields.size()];
for(int i = 0; i < EncryptTagFields.size(); ++i) {
Field field = (Field)EncryptTagFields.get(i);
String value = (String)field.get(object);
originalValues[i] = value;
}
for(int i = 0; i < EncryptTagFields.size(); ++i) {
Field field = (Field)EncryptTagFields.get(i);
String value = originalValues[i];
if (value == null) {
continue;
}
Field encryptField = getEncryptField(object, field);
if (encryptField == null) {
continue;
}
String encryptValue = encryptFieldValue(value);
encryptField.set(object, encryptValue);
field.set(object, null);
}
}
}
private static String encryptFieldValue(String value) {
String encryptValue = value + "encrypt";
return encryptValue;
}
public static void decrypt(Object object) throws Throwable {
if (object == null) {
return;
}
String className = object.getClass().getName();
List<Field> encryptTagFields = ENCRYPT_TAG_FIELDS.get(className);
if (encryptTagFields == null) {
encryptTagFields = findEncryptTagFields(object);
ENCRYPT_TAG_FIELDS.putIfAbsent(className, encryptTagFields);
}
decryptFields(object, encryptTagFields);
}
private static void decryptFields(Object object, List<Field> encryptTagFields) throws Throwable {
if (encryptTagFields.isEmpty()) {
return;
}
for (int i = 0; i < encryptTagFields.size(); ++i) {
Field field = encryptTagFields.get(i);
Field encryptField = getEncryptField(object, field);
Object fieldValue = encryptField.get(object);
if (fieldValue == null) {
continue;
}
if (fieldValue instanceof String) {
String value = (String) fieldValue;
value = AesUtil.decrypt(value);
field.set(object, value);
encryptField.set(object, null);
}
}
}
private static List<Field> findEncryptTagFields(Object object) {
Class clazz = object.getClass();
List<Field> fieldList = new ArrayList<>();
for(; clazz != null; clazz = clazz.getSuperclass()) {
Field[] declaredFields = clazz.getDeclaredFields();
int length = declaredFields.length;
for(int index = 0; index < length; ++index) {
Field field = declaredFields[index];
if (field.getAnnotation(EncryptTag.class) != null) {
if (field.getType() == String.class) {
field.setAccessible(true);
fieldList.add(field);
} else {
LOGGER.error("@EncryptTag should be used on String field. class: {}, fieldName: {}", clazz.getName(), field.getName());
}
}
}
}
return fieldList;
}
private static Field getEncryptField(Object object, Field field) throws Exception {
String encryptFieldName = AesUtil.encrypt(field.getName());
Field encyptField = getField(object, encryptFieldName);
if (encyptField == null) {
thrownew Exception(object.getClass() + "對(duì)象沒有對(duì)應(yīng)的加密字段:" + encryptFieldName);
} else {
encyptField.setAccessible(true);
return encyptField;
}
}
public static Field getField(Object object, String fieldName) {
for(Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
Field[] var3 = clazz.getDeclaredFields();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Field field = var3[var5];
if (field.getName().equals(fieldName)) {
return field;
}
}
}
returnnull;
}
}
日志脫敏方案
日志脫敏,核心在于序列化時(shí)對(duì)于敏感字段修改其序列化方式。各大序列化工具一般都有序列化自定義功能,關(guān)注公眾號(hào):碼猿技術(shù)專欄,回復(fù)關(guān)鍵詞:IDEA 獲取最新版IDEA破解腳本!本文以fastjson為例講解實(shí)現(xiàn),實(shí)現(xiàn)方式有兩種:
- 基于注解@JSONField實(shí)現(xiàn)
- 基于序列化過(guò)濾器
@JSONField方式不建議使用,對(duì)業(yè)務(wù)入侵太大。另外一種繼續(xù)序列化過(guò)濾器,fastjson提供了多種SerializeFilter
:
- PropertyPreFilter 根據(jù)PropertyName判斷是否序列化
- PropertyFilter 根據(jù)PropertyName和PropertyValue來(lái)判斷是否序列化
- NameFilter 修改Key,如果需要修改Key,process返回值則可
- ValueFilter 修改Value
- BeforeFilter 序列化時(shí)在最前添加內(nèi)容
- AfterFilter 序列化時(shí)在最后添加內(nèi)容
通過(guò)實(shí)現(xiàn)ValueFilter自定義序列化擴(kuò)展,針對(duì)目標(biāo)類以及字段進(jìn)行脫敏返回。
核心代碼簡(jiǎn)化如下:
public class FastjsonValueFilter implements ValueFilter {
@Override
public Object process(Object object, String name, Object value) {
if (needDesensitize(object, name)) {
return desensitize(value);
}
}
}
String s = JSON.toJSONString(new Person("131xxxx1552","123@163.com"),new FastjsonValueFilter());
在標(biāo)記脫敏字段以及對(duì)應(yīng)方法時(shí),可以通過(guò)配置的方法, 對(duì)類相關(guān)的脫敏字段以及方法進(jìn)行封裝。要求不高的話添加響應(yīng)的注解也可實(shí)現(xiàn)。
輸出脫敏
在輸出層織入切面進(jìn)行攔截,在切面內(nèi)實(shí)現(xiàn)脫敏邏輯。實(shí)現(xiàn)邏輯跟日志脫敏類似,需要對(duì)脫敏字段進(jìn)行標(biāo)記以及對(duì)應(yīng)脫敏方法。
如果是Spring Boot集成,配置 Spring MVC 的話只需繼承 WebMvcConfigurer
覆寫 configureMessageConverters
方法,支持全局和指定類脫敏配置,示例如下:
@Configuration
publicclass FastJsonWebSerializationConfiguration implements WebMvcConfigurer {
@Bean(name = "httpMessageConverters")
public HttpMessageConverters fastJsonHttpMessageConverters() {
// 1.定義一個(gè)converters轉(zhuǎn)換消息的對(duì)象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
// 2.添加fastjson的配置信息,比如: 是否需要格式化返回的json數(shù)據(jù)
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
// 中文亂碼解決方案
List<MediaType> mediaTypes = new ArrayList<>();
//設(shè)定json格式且編碼為UTF-8
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(mediaTypes);
//添加全局自定義脫敏
fastJsonConfig.setSerializeFilters(new ValueDesensitizeFilter());
//添加指定類脫敏方法
Map<Class<?>, SerializeFilter> classSerializeFilters = new HashMap<>();
classSerializeFilters.put(Employee.class, new FastjsonValueFilter());
fastJsonConfig.setClassSerializeFilters(classSerializeFilters);
// 3.在converter中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
// 4.將converter賦值給HttpMessageConverter
HttpMessageConverter<?> converter = fastConverter;
// 5.返回HttpMessageConverters對(duì)象
returnnew HttpMessageConverters(converter);
}
}
總結(jié)
本文總結(jié)了企業(yè)中脫敏方案實(shí)現(xiàn),包含數(shù)據(jù)庫(kù)脫敏、日志脫敏、輸出脫敏,并貼上關(guān)鍵實(shí)現(xiàn)代碼。能夠滿足業(yè)務(wù)的要求。