來自公眾號(hào):程序通事
作者:樓下小黑哥
出于安全考慮琉兜,現(xiàn)需要將數(shù)據(jù)庫(kù)的中敏感信息加密存儲(chǔ)到數(shù)據(jù)庫(kù)中,但是正常業(yè)務(wù)交互還是需要使用明文數(shù)據(jù)毙玻,所以查詢返回我們還需要經(jīng)過相應(yīng)的解密才能返回給調(diào)用方豌蟋。
?
ps:日常開發(fā)中,我們要有一定的安全意識(shí)淆珊,對(duì)于密碼夺饲,金融數(shù)據(jù)等敏感信息進(jìn)行加密存儲(chǔ)保護(hù)。
?
這個(gè)需求說起來不是很難施符,我們只需要在執(zhí)行 sql 之前,提前將指定數(shù)據(jù)進(jìn)行加密擂找。執(zhí)行 sql 之后戳吝,獲取返回結(jié)果,再進(jìn)行的相應(yīng)的解密贯涎。稍微改造下原有代碼听哭,很快完成需求。
?
現(xiàn)有加密算法如 RSA2 塘雳,AES 等陆盘,密文長(zhǎng)度將會(huì)是明文好幾倍。上線加解密方案一定要評(píng)估數(shù)據(jù)庫(kù)現(xiàn)有字段長(zhǎng)度是否滿足加密之后長(zhǎng)度败明。
?
如果這是一張新建的表隘马,上面的實(shí)現(xiàn)方案并沒有什么問題。但是這次我們改造是幾張已有已有「千萬級(jí)」的存量的數(shù)據(jù)的表妻顶,這些數(shù)據(jù)都未被加密存儲(chǔ)酸员。
如果使用上述代碼,使用加密之后的密文信息查詢歷史數(shù)據(jù)讳嘱,當(dāng)然查詢不到任何結(jié)果幔嗦。另外當(dāng)查詢返回的結(jié)果是明文,解密明文數(shù)據(jù)庫(kù)也可能會(huì)導(dǎo)致相應(yīng)的解密錯(cuò)誤沥潭。
所以為了兼容歷史數(shù)據(jù)邀泉,需要進(jìn)行如下改造:
- 增加新字段存放對(duì)應(yīng)的加密數(shù)據(jù),sql 等值條件查詢修改成 in 查詢
- 查詢返回的記錄首先判斷是否是密文钝鸽,如果是密文再去解密
代碼改造如下:
上述代碼雖然解決業(yè)務(wù)需求汇恤,但是這個(gè)解決方案不是很優(yōu)雅,業(yè)務(wù)代碼改動(dòng)較大寞埠,加解密的代碼不能通用屁置,所有涉及到相關(guān)字段的方法都需要改動(dòng),且?guī)缀醵际侵貜?fù)代碼仁连,代碼侵入性很強(qiáng)蓝角,不是很友好阱穗。
有經(jīng)驗(yàn)的同學(xué)可能會(huì)想到使用 Spring AOP 解決上述問題。
在切面的前置方法「beforeMethod」統(tǒng)一攔截查詢參數(shù)使鹅,配合自定義的注解揪阶,加密指定的字段。
然后在切面的后置方法「afterReturn」攔截返回值患朱,配合自定義注解鲁僚,解密指定的字段。
?
Spring AOP 代碼實(shí)現(xiàn)比較復(fù)雜裁厅,這里就不貼出具體的代碼冰沙。
?
但是 Spring AOP 方案也并不通用,如果其他的應(yīng)用也有相同的需求执虹,同樣的代碼拓挥,又需要重復(fù)實(shí)現(xiàn),還是很費(fèi)時(shí)費(fèi)力袋励。
最終我們參考一個(gè) github 開源項(xiàng)目「typehandlers-encrypt」侥啤,借助 mybatis 的 「TypeHandler」,實(shí)現(xiàn)通用的數(shù)據(jù)加解密解決方案茬故。使用方只需要引入相關(guān)依賴盖灸,「無需改動(dòng)一行業(yè)務(wù)代碼」,僅需少量配置即可實(shí)現(xiàn)指定字段加解密操作磺芭,省時(shí)省力赁炎。
?
「typehandlers-encrypt」 github 地址:https://github.com/drtrang/typehandlers-encrypt
?
實(shí)現(xiàn)原理
mybatis 利用內(nèi)置類型轉(zhuǎn)換器(「typeHandler」),實(shí)現(xiàn) Java 類型與 JDBC 類型的相互轉(zhuǎn)換徘跪,我們正好可以利用這個(gè)特性甘邀,在轉(zhuǎn)換之前加入加解密步驟。
typeHandler
底層原理不是復(fù)雜垮庐,如果我們沒有使用 Mybatis松邪,而是直接使用最原始的 JDBC 執(zhí)行查詢語(yǔ)句,相關(guān)代碼如下:
我們需要手動(dòng)判斷 Java 類型哨查,然后調(diào)用 PreparedStatement
設(shè)置合適類型參數(shù)逗抑。獲取返回結(jié)果之后,又需要手動(dòng)調(diào)用 ResultSet
結(jié)果集獲取相應(yīng)類型的數(shù)據(jù)寒亥,這個(gè)過程十分繁瑣邮府。
使用 mybatis 之后,上述步驟就無需我們?cè)賹?shí)現(xiàn)了溉奕。mybatis 可以通過識(shí)別 Java/JDBC 類型褂傀,調(diào)用相應(yīng)typeHandler
,自動(dòng)實(shí)現(xiàn)轉(zhuǎn)換邏輯加勤。
下圖為 mybatis 內(nèi)置類型轉(zhuǎn)換器仙辟,基本涵蓋了所有 「Java/JDBC」 數(shù)據(jù)類型同波。
通用解決方案
自定義 typeHandler
下面我們來實(shí)現(xiàn)帶有加解密功能的類型轉(zhuǎn)換器,實(shí)現(xiàn)方式也比較簡(jiǎn)單叠国,只要繼承 org.apache.ibatis.type.BaseTypeHandler
未檩,重寫相關(guān)方法。
?
簡(jiǎn)單起見粟焊,上述加解密僅使用了 Base64冤狡,大家可以替換成相應(yīng)加解密算法即或者引入相應(yīng)加解密服務(wù)。
?
其中加密轉(zhuǎn)換將在 setNonNullParameter
中執(zhí)行项棠,解密轉(zhuǎn)換將在 getNullableResult
中執(zhí)行悲雳。
CryptTypeHandler
使用一個(gè) MappedTypes
注解,包含一個(gè) CryptType
類香追,這個(gè)類使用 mybatis 別名功能怜奖,可以極大簡(jiǎn)化 sqlmap 相關(guān)配置。
注冊(cè) typeHandler
使用方必須將 typeHandler
和 alias
注冊(cè)到 mybatis 中翅阵,否則無法生效。
下面提供三種方式,可以根據(jù)項(xiàng)目情況選擇其中一種即可:
「單獨(dú)使用 mybatis」
這種場(chǎng)景需要在 「mybatis-config.xml」 配置迁央,mybatis 啟動(dòng)時(shí)將會(huì)加載該配置文件掷匠。
<typeHandlers>
<!--類型轉(zhuǎn)換器包路徑-->
<package name="com.xx.xx"/>
</typeHandlers>
<!-- 別名定義 -->
<typeAliases>
<!-- 針對(duì)單個(gè)別名定義 type:類型的路徑 alias:別名 -->
<typeAlias type="xx.xx.xx" alias="xx"/>
</typeAliases>
「使用 Spring 配置 Mybatis Bean」
配合 Spring 使用時(shí)需要將 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 方式就最簡(jiǎn)單了岖圈,只要引入 mybatis-starter
讹语,配置文件加入如下配置即可:
## mybatis 配置
# 類型轉(zhuǎn)換器包路徑
mybatis.type-handlers-package=com.xx.xx.x
mybatis.type-aliases-package=com.xx.xx
修改 mapper sql 配置
最后我們只要簡(jiǎn)單修改 mapper 中 resultMap
或 sql s配置就可以實(shí)現(xiàn)加解密。
假設(shè)我們對(duì)現(xiàn)有一張 「bank_card」 表進(jìn)行加解密蜂科,表結(jié)構(gòu)如下:
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 '卡號(hào)',
phone varchar(256) NOT NULL DEFAULT '' COMMENT '手機(jī)號(hào)',
name varchar(256) NOT NULL DEFAULT '' COMMENT '姓名',
id_no varchar(256) NOT NULL DEFAULT '' COMMENT '證件號(hào)'
);
「insert 加密」
現(xiàn)需要對(duì) card_no
顽决,phone
,name
导匣,id_no
進(jìn)行加密才菠,「insert」 語(yǔ)句加密示例:
<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,傳入?yún)?shù)最后將被加密贡定。使用 typeHandler
需要使用類的全路徑赋访,比較繁瑣,我們可以使用 「javaType」 屬性缓待,直接使用上面我們的定義別名 「crypt」蚓耽。
數(shù)據(jù)庫(kù)最終執(zhí)行sql 如下:
INSERT INTO bank_card (card_no, phone,name,id_no) VALUES ('NjQzMjEyMzEyMzE=', 'MTM1Njc4OTEyMzQ=', '5rWL6K+V5Y2h', 'MTIzMTIzMTIzMQ==');
?
ps:推薦一款 IDEA 的插件 「mybatis-log-plugin」,可以自動(dòng)將 mybatis sql 日志還原成真實(shí)執(zhí)行 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>
這里我們?cè)?「select」 配置中只能使用 resultMap
屬性旋炒,指定 typeHandler
步悠。
數(shù)據(jù)庫(kù)明文、密文共存的情況瘫镇,查詢解密示例如下:
<!-- resultMap 同上 -->
<select id="queryByPhone" resultMap="bankCardXml">
select * from bank_card where phone in(#{card_no,javaType=crypt},#{card_no})
</select>
最后我們可以將自定義的 typeHandler
單獨(dú)打包發(fā)布鼎兽,其他業(yè)務(wù)方只需要引用答姥,改造相關(guān)配置文件,即可完成數(shù)據(jù)加解密接奈。
上述代碼示例已上傳至 Github踢涌,地址:https://github.com/9526xu/mybatis-encrypt
總結(jié)
借助于自定義的 typeHandler
,我們實(shí)現(xiàn)了一個(gè)通用的加解密的方案序宦,該方案對(duì)于使用方來說代碼侵入性小睁壁,開箱即用,可以快速完成加解密的改造互捌。
?
ps:你們是否也有遇到同樣的需求潘明,可以在下方留言寫下你們的方案,互相學(xué)習(xí)秕噪,一起成長(zhǎng)钳降!
?
最后感謝一下@輝哥提供解決思路。
Reference
最后
看到這里腌巾,想必大家都累了遂填,放一張趣圖輕松一下。