MyBatis實現字段加解密的實踐
背景
互聯(lián)網系統(tǒng)充斥著各種敏感信息,包括各種個人信息、商業(yè)信息等等。按照要求,不允許隱私信息明文存儲,需要進行加密處理,防止造成隱私泄露的風險。
我司作為一個跨境電商公司,各系統(tǒng)中,自然免不了涉及各類敏感信息,并且對各類敏感信息的安全級別進行了劃分,不同等級的加密要求級別有一定的差別。
方案
由于不同的敏感數據需要使用不同的加解密策略,在MySQL層面,無法滿足需求,所以只能在應用代碼層面進行實現。但需要考慮幾個點
- 盡可能減少對業(yè)務代碼侵入性;
- 以最小的風險進行改動;
- 方案可復用,方便拓展;
手動加解密
這是首先被提出來的方案。該方案做法是
- 在任何涉及到讀取敏感字段的業(yè)務代碼中,進行手動解密操作。
- 在任何涉及到寫入敏感字段的業(yè)務代碼中,進行手動加密操作。
優(yōu)點:
- 暫無;
缺點:
- 侵入業(yè)務代碼,業(yè)務開發(fā)甚至需要關注不同級別的加解密策略;
- 老舊系統(tǒng)調用復雜,可能出現重復加密或重復解密,導致無法復原原始數據,風險數據非常高;
- 若數據安全級別變動,加密策略升級,所有可能涉及到的業(yè)務代碼都需要變更,風險半徑無法預測;
綜上所述,該方案完全無法滿足我們對方案的要求,屬于最笨的方案,可以直接say no。
自動加解密
由于無法在MySQL層面實現,又希望盡可能減少對業(yè)務代碼的侵入性,那么任務只能落在ORM框架或半ORM框架上。我們系統(tǒng)使用的是MyBatis,那么利用MyBatis的插件機制來實現自動加解密,是個不錯的選擇。
優(yōu)點:
- 業(yè)務代碼無需改造,幾乎對業(yè)務代碼無入侵性;
- 加解密統(tǒng)一入口,風險可控;
- 方案多個系統(tǒng)通用,加解密策略可隨時拓展;
缺點:
- 暫無;
實現
編碼
由于敏感數據被劃分成多個不同級別,各個級別使用的加解密算法不同,所以面對這種不同加密算法的場景,策略模式非常適合;
加解密策略
public interface SensitiveStrategy { /** * 加密 */ String encrypt(String value); /** * 解密 */ String decrypt(String value); }
各種加解密算法,只要實現該接口即可,此處略。
自定義注解
自定義一個字段上的注解,目的是為了讓MyBatis攔截器識別哪些字段需要加解密,加解密的策略是什么。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) public @interface SensitiveField { Class<? extends SensitiveStrategy> sensitiveStrategy(); }
MyBatis攔截器
我們需要定義一個MyBatis攔截器,該攔截器的作用有以下:
- 攔截入參,識別被@SensitiveField注解的字段,并使用指定加密策略,對字段內容進行加密;
- 攔截查詢結果,識別被@SensitiveField注解的字段,并使用指定的解密策略,對字段內容進行解密;
我們知道,MyBatis的攔截器插件,可以對四大組件Executor、StatementHandler、ParameterHandler、ResultSetHandler進行攔截,由于我們只需要對入參和結果進行攔截和修改,所以只需指定攔截ParameterHandler、ResultSetHandler即可。
@Slf4j @Component @Intercepts(value = { @Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class}), @Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class}) }) public class SensitiveInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); //入參加密 if (target instanceof ParameterHandler parameterHandler){ //獲取參數對象 Object parameterObj = ReflectUtil.getFieldValue(parameterHandler, "parameterObject"); if (parameterObj != null){ //獲取參數對象內的字段 Arrays.stream(ReflectUtil.getFields(parameterObj.getClass())) .filter(field -> String.class.equals(field.getType())) .filter(field -> field.getAnnotation(SensitiveField.class)!=null ) .filter(field -> ReflectUtil.getFieldValue(parameterObj,field) != null) .forEach(field -> { SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class); Class<? extends SensitiveStrategy> strategyClazz = sensitiveField.sensitiveStrategy(); if (strategyClazz != null){ SensitiveStrategy strategy = SpringContext.getBean(strategyClazz); String encrypt = strategy.encrypt(ReflectUtil.getFieldValue(parameterObj, field).toString()); ReflectUtil.setFieldValue(parameterObj,field,encrypt); } }); ReflectUtil.setFieldValue(parameterHandler,"parameterObject",parameterObj); } } Object resultObj = invocation.proceed(); //出參解密 if (resultObj != null && target instanceof ResultSetHandler){ List<?> resultList = (List<?>) resultObj; for (Object result : resultList) { if (!SimpleTypeRegistry.isSimpleType(result.getClass())){ Arrays.stream(ReflectUtil.getFields(result.getClass())) .filter(field -> String.class.equals(field.getType())) .filter(field -> field.getAnnotation(SensitiveField.class)!=null ) .filter(field -> ReflectUtil.getFieldValue(result,field) != null) .forEach(field -> { SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class); Class<? extends SensitiveStrategy> strategyClazz = sensitiveField.sensitiveStrategy(); if (strategyClazz != null){ SensitiveStrategy strategy = SpringContext.getBean(strategyClazz); String decrypt = strategy.decrypt(ReflectUtil.getFieldValue(result, field).toString()); ReflectUtil.setFieldValue(result,field,decrypt); } }); } } resultObj = resultList; } return resultObj; } @Override public Object plugin(Object target) { return Plugin.wrap(target,this); } }
由于系統(tǒng)是基于SpringBoot的,所以我們將所有實現的加解密策略對象,交給Spring容器管理。在MyBatis攔截器中,直接從Spring容器中獲取對應加解密策略使用接口。
上線
上線階段,我們分為以下幾個步驟實施
- 臨時關閉敏感數據列的修改功能入口,避免出現備份后源數據變更;
- 備份敏感字段列數據,小表DBA直接備份,大表編寫代碼分批備份;
- 利用apollo控制讀取時不解密,寫入時加密,對對應字段進行一次讀取后更新即可完成加密;
- 觀察涉及的業(yè)務功能讀取是否正常;
- 開啟敏感數據修改功能入口,觀察系統(tǒng)是否正常;
其他問題
其實在整個過程中,實現功能相關的編碼并不復雜。除此之外,需要去識別并解決其他的一些問題,這期間花費了更多的時間,例如
敏感字段like搜索
字段加密后存儲,就無法直接使用like關鍵字搜索。其實經過我司安全部門評估,敏感字段也不允許進行模糊搜索,有數據泄露風險,所以在產品方案層面,直接砍掉了類似功能;
若是一定要保留改功能,也可以使用以下方案
- 拓展一個新字段,用于模糊搜索,類型為text;
- 對原始字符進行分割,可按每N個字符為一項進行分割,分割后每一項使用固定加密算法加密,再使用固定字符對每一項進行拼接,形成新的字符串,保存到新字段;
- like查詢時,同樣也是分割、加密、拼接的方式;
注意,like字段需要使用text類型,性能會很低。
敏感字段group by
對于一些敏感級別較低的字段,采用了固定加密方式(即多次對相同的數據進行加密后結果不變),此時由于結果不變,可以直接使用加密后的字段進行group by;
但是對于敏感級別較高的字段,我司采用了動態(tài)加密方式(即多次對相同的數據加密結果不一致),此時由于結果不一致,無法進行group by操作。解決方案的拓展多一列,對源數據使用固定加密后,將結果存進該拓展字段,group by業(yè)務使用。
到此這篇關于MyBatis實現字段加解密的實踐的文章就介紹到這了,更多相關MyBatis 字段加解密內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springcloud?nacos動態(tài)線程池Dynamic?tp配置接入實戰(zhàn)詳解
這篇文章主要為大家介紹了springcloud?nacos動態(tài)線程池Dynamic?tp配置接入實戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12java.lang.ExceptionInInitializerError異常的解決方法
這篇文章主要為大家詳細介紹了java.lang.ExceptionInInitializerError異常的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10關于Java錯誤提示之找不到或無法加載主類的問題及正確處理方法
當我們在初學Java的是時候,類文件中是不設定包名(package)的,這種情況下注意classpath,基本上沒有問題,?本文主要說明classpath和系統(tǒng)環(huán)境變量PATH都沒問題的情況下出錯原因和正確處理方法,感興趣的朋友一起看看吧2022-01-01Jvisualvm監(jiān)控遠程SpringBoot項目的過程詳解
這篇文章主要介紹了Jvisualvm監(jiān)控遠程SpringBoot項目,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-04-04使用SpringBoot+Prometheus+Grafana實現可視化監(jiān)控
本文主要給大家介紹了如何使用Spring?actuator+監(jiān)控組件prometheus+數據可視化組件grafana來實現對Spring?Boot應用的可視化監(jiān)控,文中有詳細的代碼供大家參考,具有一定的參考價值,需要的朋友可以參考下2024-02-02