---
title: 在线系统脱敏改造实战：堪比“空中换引擎”
description: 随着对个人信息保护意识的增强，如今，但凡在系统里出现身份证号、手机号或银行卡号等各类敏感信息，开发者均会进行加密或哈希算法处理后再储存，这已经是一个必备常识。然而，在开发实践中，我们依旧不可避免地会遇到“历史遗留问题”——系统中被忘记加密的明文敏感字段。
---

<https://blog.advance.ai/zh-cn>

# [在线系统脱敏改造实战：堪比“空中换引擎”](https://blog.advance.ai/zh-cn/online-system-desensitization-transformation-practice)

 作者：[Sample HubSpot User](https://blog.advance.ai/zh-cn/author/sample-hubspot-user) | Jul 16, 2021, 1:18:00 PM

作者：杜红军

随着对个人信息保护意识的增强，如今，但凡在系统里出现身份证号、手机号或银行卡号等各类敏感信息，开发者均会进行加密或哈希算法处理后再储存，这已经是一个必备常识。然而，在开发实践中，我们依旧不可避免地会遇到“历史遗留问题”——系统中被忘记加密的明文敏感字段。  
如果这些明文敏感字段，出现在核心服务系统里，这个系统“脱敏”改造工程将会变得十分棘手——一是因为核心系统的服务通常不能随便中断；二是由于核心系统的服务往往已应用广泛，数据量巨大，要给这样不能停的系统进行“脱敏”改造，其难度相当于要给飞行中的飞机换发动机！ 结合一次“空中修引擎”的亲身经历，本篇博文带大家一起探讨在线系统 如何进行脱敏改造，让我们一起挑战、破解难题中的难题。

 

## 一、 确定脱敏数据的存储形式

敏感数据虽然在储存时要加密，但在使用时需要被转换成明文，因此，加密的过程必须是可逆的。针对在线系统中出现的明文敏感数据，我们选择了被业界广泛接受的AES256-CBC形式，增加了一个保存加密信息的字段 xxx\_secure。然而，由于随机初始向量(IV)的存在，会出现同一明文敏感数据多次加密后，产生多个完全不同的密文，导致这个密文字段不可进行索引或是进行直接比对，因此，我们又额外引入了一个哈希字段 xxx\_hash 用于检索。

 

## 二、 选择数据对象的映射方式

此次进行脱敏改造的在线系统部署了较好的ORM映射管理，使用了数据持久化JPA框架，基本上没有裸读写数据库的情况，因此，本次改造只需要在ORM框架层面改变实体对象的读写行为，让其变成访问相应的加密字段，即可保持对象对既有业务行为的一致，无需改动业务代码——这是本次改造实战中最值得庆幸的一件事，也从侧面再次证明数据访问层（DAO）的价值——即使是再薄的一个框架，如果自带DAO层，即使系统出现严重问题，也能带来更多切入挽救的机会。

JPA框架下通常有以下两种方式实现数据对象映射：

1\. 定义converter实现AttributeConverter接口双向转化  
该接口是范型实现，必须要存在2个范型类型，分别对应JAVA类型和SQL类型，并一定要求数据可以双向转化，我们在公共包里实现了一个 Encrypted AttributeConverter类型，其主要相关方法如下：

△图1：代码诠释

随后，在Entity属性上使用Convert注解，指定上述converter，即可在读写数据库时实现自动加密和解密，参考如下：

△图2：代码诠释

2\. 实现自定义用户类（UserType）接口   
自定义用户类接口适用于需要自动化转化哈希加密的情形，可以有效避免双向数据可逆的影响。因为JPA框架在持久化的过程中需要通过深拷贝方式克隆原始对象副本，若使用AttributeConverter接口转化哈希加密，则AttributeConverterMutabilityPlanImpl.deepCopyNotNull(T value)是深拷贝的入口，这个克隆方向最终会调用AttributeConverter.convertToEntityAttribute(String dbData)的数值来复制对象（参见“图1代码诠释”），但由于哈希加密不可逆，如果数据库数据是哈希值则无法获取明文。而当保存数据时，又会调用convertToDatabaseColumn(HashStringField attribute) 对数据做加密处理，若此时明文字段处为空字符串，则最终会导致数据丢失。  
自定义用户类接口可以通过Override深拷贝方法实现JAVA type到SQL type的数据映射，该接口定义有很多方法，比如指明JAVA 类型；指明SQL 类型；SQL数据到JAVA 对象之间的互转；或者对象标识符方法，如hashCode()和equals()方法；深拷贝方法及序列化相关方法, 以上方法具体代码诠释示意如下（图3）：

△图3：代码诠释

自定义用户数据接口实现之后，就可以在Entity类的属性中使用类型注解，指明属性，例如：

△图4：代码诠释

然后，就可以实现自定义用户数据类型的单向转化。但是在实践中，我们认为这个方法过于复杂，而且把明明是一个String的哈希字符串强行定义为新类型HashStringField也很怪异，经过多方考量，最终还是选择通过在getter、setter方法直接转化，而没有用自定义用户接口方案实现数据映射。

 

## 三、 开始“空中换引擎”——在线无缝脱敏

完成明文字段的自动转化，在确保业务逻辑无需大量变更之后，还要面对服务多节点多区域部署的问题，因为此时线上无可避免地会出现新旧服务并存的阶段，但由于是核心系统，不可能安排停机更新。那么，如何为运行中的系统“空中换引擎”？综合而言，为数据库表添加脱敏字段后，我们在实践中实施了三步替换方案：第一步：双写但是信任明文字段；第二步：双写但是信任密文字段；第三步：单独使用密文字段，完成后在数据库中废除明文字段，消除安全隐患。以上步骤具体实施情况如下：

第一步：双写但是信任明文字段  
以Entity中的phone字段为例，首先向数据库添加相关脱敏字段，如phone\_hash、phone\_secure，内容可默认为空。接着，更新Entity对象并升级服务，服务获取数据时以明文字段为准，但写入时，同时更新明文和密文字段，保证升级过程中，读取的数据是准确的，再通过相关getter、setter方法转化。

△图4：代码诠释

第二步：双写但是信任密文字段  
脱敏字段更新完成后，检查所有服务功能，确认升级成功。再运行脚本， set phone\_secure = Encrypt(phone) ，补齐加密字段所有内容，此时 phone\_secure字段中即为全量正确的加密后的数据。  
然后，修改代码 ，切换信任关系，由于服务多节点、跨地域存在，此时程序需要仍然保持双写，保证新旧节点读取数据都是最新值，但是对象取值以加密字段为准，继续以phone举例，操作如下：

△图5：代码诠释

第三步：单独使用密文字段  
第二步完成后，系统中所有节点已信任加密文字段，即可以进入解除对明文字段写入的步骤——单独使用密文字段，并准备从系统中废弃明文字段。此时，需要在Entity中修改映射关系的方式如下：

至此，Entity 类已恢复为干净的实体类别，并且整体看代码无其他太多变化，只是在phone字段增添了两个注解。删除明文字段之后，经一段时长的运行观察均状况正常之后，可以先重命名，然后彻底删除明文字段——“空中修引擎”般的在线系统脱敏工程宣告完成。

 

[查看完整帖子](https://blog.advance.ai/zh-cn/online-system-desensitization-transformation-practice)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Sample HubSpot User"
  },
  "dateModified" : "2023-11-15T09:33:17.676Z",
  "datePublished" : "2021-07-16T13:18:00Z",
  "headline" : "在线系统脱敏改造实战：堪比“空中换引擎”",
  "image" : {
    "@type" : "ImageObject",
    "height" : 1280,
    "url" : "https://f.hubspotusercontent10.net/hubfs/5877117/shutterstock_1447718978.jpg",
    "width" : 1920
  },
  "mainEntityOfPage" : "https://blog.advance.ai/zh-cn/online-system-desensitization-transformation-practice",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60,
      "url" : "/hs/hsstatic/content_shared_assets/static-1.4092/img/default-amp-logo.png",
      "width" : 60
    },
    "name" : "Chinese Blog"
  }
}
```