驚呆了!不改一行 Java 程式碼竟然就能輕鬆解決敏感資訊加解密|原創
前言
出於安全考慮,現需要將資料庫的中敏感資訊加密儲存到資料庫中,但是正常業務互動還是需要使用明文資料,所以查詢返回我們還需要經過相應的解密才能返回給呼叫方。
ps:日常開發中,我們要有一定的安全意識,對於密碼,金融資料等敏感資訊事實加密儲存保護。
這個需求說起來不是很難,我們只需要在執行 sql 之前,提前將指定資料進行加密。執行 sql 之後,獲取返回結果,再進行的相應的解密。稍微改造下原有程式碼,很快完成需求。
現有加密演算法如 RSA2 ,AES 等,密文長度將會是明文好幾倍。上線加解密方案一定要評估資料庫現有欄位長度是否滿足加密之後長度。
如果這是一張新建的表,上面的實現方案並沒有什麼問題。但是這次我們改造是幾張已有已有千萬級的存量的資料的表,這些資料都未被加密儲存。
如果使用上述程式碼,使用加密之後的密文資訊查詢歷史資料,當然查詢不到任何結果。另外當查詢返回的結果是明文,解密明文資料庫也可能會導致相應的解密錯誤。
所以為了相容歷史資料,需要進行如下改造:
- 增加新欄位存放對應的加密資料,sql 等值條件查詢修改成 in 查詢
- 查詢返回的記錄首先判斷是否是密文,如果是密文再去解密
程式碼改造如下:
上述程式碼雖然解決業務需求,但是這個解決方案不是很優雅,業務程式碼改動較大,加解密的程式碼不能通用,所有涉及到相關欄位的方法都需要改動,且幾乎都是重複程式碼,程式碼侵入性很強,不是很友好。
有經驗的同學可能會想到使用 Spring AOP 解決上述問題。
在切面的前置方法(beforeMethod)統一攔截查詢引數,配合自定義的註解,加密指定的欄位。
然後在切面的後置方法(afterReturn)攔截返回值,配合自定義註解,解密指定的欄位。
Spring AOP 程式碼實現比較複雜,這裡就不貼出具體的程式碼。
但是 Spring AOP 方案也並不通用,如果其他的應用也有相同的需求,同樣的程式碼,又需要重複實現,還是很費時費力。
最終我們參考一個 github 開源專案『typehandlers-encrypt』,藉助 mybatis 的 TypeHandler,實現通用的資料加解密解決方案。使用方只需要引入相關依賴,無需改動一行業務程式碼,僅需少量配置即可實現指定欄位加解密操作,省時省力。
typehandlers-encrypt github 地址:https://github.com/drtrang/typehandlers-encrypt
實現原理
mybatis 利用內建型別轉換器(typeHandler),實現 Java 型別與 JDBC 型別的相互轉換,我們正好可以利用這個特性,在轉換之前加入加解密步驟。
typeHandler
底層原理不是複雜,如果我們沒有使用 Mybatis,而是直接使用最原始的 JDBC 執行查詢語句,相關程式碼如下:
我們需要手動判斷 Java 型別,然後呼叫 PreparedStatement
設定合適型別引數。獲取返回結果之後,又需要手動呼叫 ResultSet
結果集獲取相應型別的資料,這個過程十分繁瑣。
使用 mybatis 之後,上述步驟就無需我們再實現了。mybatis 可以通過識別 Java/JDBC 型別,呼叫相應typeHandler
,自動實現轉換邏輯。
下圖為 mybatis 內建型別轉換器,基本涵蓋了所有 Java/JDBC 資料型別。
通用解決方案
自定義 typeHandler
下面我們來實現帶有加解密功能的型別轉換器,實現方式也比較簡單,只要繼承 org.apache.ibatis.type.BaseTypeHandler
,重寫相關方法。
簡單起見,上述加解密僅使用了 Base64,大家可以替換成相應加解密演算法即或者引入相應加解密服務。
其中加密轉換將在 setNonNullParameter
中執行,解密轉換將在 getNullableResult
中執行。
CryptTypeHandler
使用一個 MappedTypes
註解,包含一個 CryptType
類,這個類使用 mybatis 別名功能,可以極大簡化 sqlmap 相關配置。
註冊 typeHandler
使用方必須將 typeHandler
和 alias
註冊到 mybatis 中,否則無法生效。
下面提供三種方式,可以根據專案情況選擇其中一種即可:
單獨使用 mybatis
這種場景需要在 mybatis-config.xml 配置,mybatis 啟動時將會載入該配置檔案。
<typeHandlers>
<!--型別轉換器包路徑-->
<package name="com.xx.xx"/>
</typeHandlers>
<!-- 別名定義 -->
<typeAliases>
<!-- 針對單個別名定義 type:型別的路徑 alias:別名 -->
<typeAlias type="xx.xx.xx" alias="xx"/>
</typeAliases>
使用 Spring 配置 Mybatis Bean
配合 Spring 使用時需要將 typeHandler
注入 SqlSessionFactoryBean
,配置方式如下:
<!-- MyBatis 工廠 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--alias 注入-->
<property name="typeAliasesPackage" value="xx.xx.xx"/>
<!-- typeHandlers 注入 -->
<property name="typeHandlersPackage" value="xx.xx.xx"/>
</bean>
SpringBoot
SpringBoot 方式就最簡單了,只要引入 mybatis-starter
,配置檔案加入如下配置即可:
## mybatis 配置
# 型別轉換器包路徑
mybatis.type-handlers-package=com.xx.xx.x
mybatis.type-aliases-package=com.xx.xx
修改 mapper sql 配置
最後我們只要簡單修改 mapper 中 resultMap
或 sql s配置就可以實現加解密。
假設我們對現有一張 bank_card 表進行加解密,表結構如下:
CREATE TABLE bank_card (
id int primary key auto_increment,
gmt_create timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_update timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
card_no varchar(256) NOT NULL DEFAULT '' COMMENT '卡號',
phone varchar(256) NOT NULL DEFAULT '' COMMENT '手機號',
name varchar(256) NOT NULL DEFAULT '' COMMENT '姓名',
id_no varchar(256) NOT NULL DEFAULT '' COMMENT '證件號'
);
insert 加密
現需要對 card_no
,phone
,name
,id_no
進行加密,insert 語句加密示例:
<insert id="insertBankCard" keyProperty="id" useGeneratedKeys="true" parameterType="org.demo.pojo.BankCardDO">
INSERT INTO bank_card (card_no, phone,name,id_no)
VALUES
(#{card_no,javaType=crypt},
#{phone,typeHandler=org.demo.type.CryptTypeHandler},
#{name,javaType=crypt},
#{id_no,javaType=crypt})
</insert>
我們只需要在 #{} 指定 typeHandler,傳入引數最後將被加密。使用 typeHandler
需要使用類的全路徑,比較繁瑣,我們可以使用 javaType 屬性,直接使用上面我們的定義別名 crypt。
資料庫最終執行sql 如下:
INSERT INTO bank_card (card_no, phone,name,id_no) VALUES ('NjQzMjEyMzEyMzE=', 'MTM1Njc4OTEyMzQ=', '5rWL6K+V5Y2h', 'MTIzMTIzMTIzMQ==');
ps:推薦一款 IDEA 的外掛 mybatis-log-plugin,可以自動將 mybatis sql 日誌還原成真實執行 sql
查詢加解密
普通查詢解密示例如下:
<resultMap id="bankCardXml" type="org.demo.pojo.BankCardDO">
<result property="card_no" column="card_no" typeHandler="org.demo.type.CryptTypeHandler"/>
<result property="name" column="name" typeHandler="org.demo.type.CryptTypeHandler"/>
<result property="id_no" column="id_no" typeHandler="org.demo.type.CryptTypeHandler"/>
<result property="phone" column="phone" typeHandler="org.demo.type.CryptTypeHandler"/>
</resultMap>
<select id="queryById" resultMap="bankCardXml">
select * from bank_card where id=#{id}
</select>
這裡我們在 select 配置中只能使用 resultMap
屬性,指定 typeHandler
。
資料庫明文、密文共存的情況,查詢解密示例如下:
<!-- resultMap 同上 -->
<select id="queryByPhone" resultMap="bankCardXml">
select * from bank_card where phone in(#{card_no,javaType=crypt},#{card_no})
</select>
最後我們可以將自定義的 typeHandler
單獨打包釋出,其他業務方只需要引用,改造相關配置檔案,即可完成資料加解密。
上述程式碼示例已上傳至 Github,地址:https://github.com/9526xu/mybatis-encrypt
總結
藉助於自定義的 typeHandler
,我們實現了一個通用的加解密的方案,該方案對於使用方來說程式碼侵入性小,開箱即用,可以快速完成加解密的改造。
ps:你們是否也有遇到同樣的需求,可以在下方留言寫下你們的方案,互相學習,一起成長!
最後感謝一下@輝哥提供解決思路。
Reference
- https://github.com/9526xu/mybatis-encrypt
- https://github.com/drtrang/typehandlers-encrypt
最後(求關注)
看到這裡,想必大家都累了,放一張趣圖輕鬆一下。
最後再次感謝您的閱讀,我是樓下小黑哥,一位還未禿頭的工具猿,下篇文章我們再見~
歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn