驚呆了!不改一行 Java 程式碼竟然就能輕鬆解決敏感資訊加解密|原創

file

前言

出於安全考慮,現需要將資料庫的中敏感資訊加密存儲到資料庫中,但是正常業務交互還是需要使用明文數據,所以查詢返回我們還需要經過相應的解密才能返回給調用方。

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 執行查詢語句,相關程式碼如下:

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 相關配置。

alias

註冊 typeHandler

使用方必須將 typeHandleralias 註冊到 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_nophonenameid_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

  1. https://github.com/9526xu/mybatis-encrypt
  2. https://github.com/drtrang/typehandlers-encrypt

最後(求關注)

看到這裡,想必大家都累了,放一張趣圖輕鬆一下。

當你在 github 提交相關 issue 期待其他人回答解答問題時

最後再次感謝您的閱讀,我是樓下小黑哥,一位還未禿頭的工具猿,下篇文章我們再見~

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn