惊呆了!不改一行 Java 代码竟然就能轻松解决敏感信息加解密|原创

来自公众号:程序通事
作者:楼下小黑哥

出于安全考虑,现需要将数据库的中敏感信息加密存储到数据库中,但是正常业务交互还是需要使用明文数据,所以查询返回我们还需要经过相应的解密才能返回给调用方。

ps:日常开发中,我们要有一定的安全意识,对于密码,金融数据等敏感信息进行加密存储保护。

这个需求说起来不是很难,我们只需要在执行 sql 之前,提前将指定数据进行加密。执行 sql 之后,获取返回结果,再进行的相应的解密。稍微改造下原有代码,很快完成需求。

image

现有加密算法如 RSA2 ,AES 等,密文长度将会是明文好几倍。上线加解密方案一定要评估数据库现有字段长度是否满足加密之后长度。

如果这是一张新建的表,上面的实现方案并没有什么问题。但是这次我们改造是几张已有已有「千万级」的存量的数据的表,这些数据都未被加密存储。

如果使用上述代码,使用加密之后的密文信息查询历史数据,当然查询不到任何结果。另外当查询返回的结果是明文,解密明文数据库也可能会导致相应的解密错误。

所以为了兼容历史数据,需要进行如下改造:

  • 增加新字段存放对应的加密数据,sql 等值条件查询修改成 in 查询
  • 查询返回的记录首先判断是否是密文,如果是密文再去解密

代码改造如下:

image

上述代码虽然解决业务需求,但是这个解决方案不是很优雅,业务代码改动较大,加解密的代码不能通用,所有涉及到相关字段的方法都需要改动,且几乎都是重复代码,代码侵入性很强,不是很友好。

有经验的同学可能会想到使用 Spring AOP 解决上述问题。

在切面的前置方法「beforeMethod」统一拦截查询参数,配合自定义的注解,加密指定的字段。

然后在切面的后置方法「afterReturn」拦截返回值,配合自定义注解,解密指定的字段。

Spring AOP 代码实现比较复杂,这里就不贴出具体的代码。

但是 Spring AOP 方案也并不通用,如果其他的应用也有相同的需求,同样的代码,又需要重复实现,还是很费时费力。

最终我们参考一个 github 开源项目「typehandlers-encrypt」,借助 mybatis 的 「TypeHandler」,实现通用的数据加解密解决方案。使用方只需要引入相关依赖,「无需改动一行业务代码」,仅需少量配置即可实现指定字段加解密操作,省时省力。

「typehandlers-encrypt」 github 地址:https://github.com/drtrang/typehandlers-encrypt

image

实现原理

mybatis 利用内置类型转换器(「typeHandler」),实现 Java 类型与 JDBC 类型的相互转换,我们正好可以利用这个特性,在转换之前加入加解密步骤。

typeHandler 底层原理不是复杂,如果我们没有使用 Mybatis,而是直接使用最原始的 JDBC 执行查询语句,相关代码如下:

image

我们需要手动判断 Java 类型,然后调用 PreparedStatement设置合适类型参数。获取返回结果之后,又需要手动调用 ResultSet 结果集获取相应类型的数据,这个过程十分繁琐。

使用 mybatis 之后,上述步骤就无需我们再实现了。mybatis 可以通过识别 Java/JDBC 类型,调用相应typeHandler,自动实现转换逻辑。

下图为 mybatis 内置类型转换器,基本涵盖了所有 「Java/JDBC」 数据类型。

image

通用解决方案

自定义 typeHandler

下面我们来实现带有加解密功能的类型转换器,实现方式也比较简单,只要继承 org.apache.ibatis.type.BaseTypeHandler,重写相关方法。

简单起见,上述加解密仅使用了 Base64,大家可以替换成相应加解密算法即或者引入相应加解密服务。

image

其中加密转换将在 setNonNullParameter 中执行,解密转换将在 getNullableResult中执行。

CryptTypeHandler 使用一个 MappedTypes 注解,包含一个 CryptType 类,这个类使用 mybatis 别名功能,可以极大简化 sqlmap 相关配置。

image

注册 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

最后

看到这里,想必大家都累了,放一张趣图轻松一下。

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279