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