[点晴永久免费OA]敏感字段信息数据库加密方案实践:我们选的不是最完美,但是真的够用了
当前位置:点晴教程→点晴OA办公管理信息系统
→『 经验分享&问题答疑 』
为什么需要加密存储?我们之前公司开发了一款社交类 App,准备上线各大应用市场的时候,突然发现上架被拒了。原因是:平台要求提供“公安备案号”,否则 App 无法审核通过,甚至有下架风险。 没办法,我们只好去注册地的网警办跑备案。结果拿到网警办的审核要求后,发现其中有一条写得特别明确: ❝ 当时我们一看就傻了。密码确实加密了,用的是常规的哈希算法,但像手机号、身份证、邮箱这些,全都明文躺在数据库里。 其实我们不是不知道加密这事有多重要,而是当初在设计的时候,根本就「没往那方面想过」。 一是觉得——「没必要啊」。数据库又不是对外开放的,正常情况下谁能碰到这些数据?除非我们服务器被攻破了,否则怎么可能泄漏? 二是我们页面上「本来也做了脱敏」。手机号、身份证这些信息,在前端或后端接口返回的时候都会做遮盖,后台操作人员看不到全量数据。就算有人越权进后台,也拿不到完整信息,我们当时觉得这就够了。 三是,「加密太麻烦了」。像用户登录、注册、实名认证这些流程,本来就已经够复杂了,偶尔还得救个急、处理失败回调什么的。再加一层加密,前端传参、后端校验、数据库查询,全得改一轮,系统复杂度直接上天。而且加密之后,什么模糊查询、手机号筛选、关键词匹配基本全废,干脆就没往那块考虑。 结果这些“我们以为不会出问题”的地方,最后全变成了卡我们脖子的点。 我们又花了一周时间重新设计字段、做加密、兼容查询逻辑,才终于顺利通过了联网备案。 也正是从这次教训之后,我们开始系统性地去思考一件事: 「敏感数据,到底该怎么加密存储,才能既安全又好用?」 手机号加密怎么做?我们是这样设计的在整改的时候,我们首先把用户信息里的手机号字段「单独拆了出来」,放到一张新的表里做专门的加密设计。 当时也确实花了不少时间研究各种方案。网上有很多资料,有些看起来挺先进的,但实际落地的时候问题一堆。 比如,有的说可以搞一个“明文映射表”,用户手机号存明文,然后另起一张加密表做关联。 还有的方案是说把手机号拆成三段,中间部分加密、首尾保留,甚至还有人搞什么正则分词加密索引的。我们看完之后的感受是:「太复杂了,维护成本太高,不适合我们业务节奏」。 我们最后回过头来,还是「从我们自己系统的使用场景出发来设计」,因为我们对手机号的使用,其实就这几种:
所以我们的思路是: ❝ 「我们做了一个更实际、更折中的方案:受控拆分字段 + 明文索引 + 模糊脱敏策略。」 说白了就是:哪些字段该加密就加密,哪些该查的我们用结构化字段保留一小部分明文, 确定了要做“加密 + 可查询”之后,我们就开始重新设计手机号的存储结构。 我们把手机号从原来的
里面的字段其实不复杂,核心就这几个:
「为什么要设计这么多字段?它们都是干啥的?」 我们最开始也纠结:只存密文不行吗?干嘛还要搞什么索引值、前缀、后缀? 后来发现,不行。想要安全,就要加密;想要查,就要加索引;想要模糊,就得拆字段——这就是结合场景考虑。 我这边简单说下我们当时为什么要这样设计这些字段。 「phone_cipher / phone_iv / phone_tag」 这三个是一组,加密手机号用的。我们用了 AES-GCM 模式做加密,需要带上随机的 IV 和 tag,用来保证密文的完整性。加密之后得到的结果就是 这个字段组主要用于后续「需要展示或导出的场景」,比如我们这边要导出用户手机号给运营或者客服留存时,就需要解密手机号再导出。 「phone_idx」 这个字段是用手机号原文计算出来的 HMAC 值,用于等值查询。不可逆,也就是说别人拿到这个字段是没法还原手机号的,但如果我们知道原手机号,在应用层也能算出一样的值,就能查出来是谁。 我们所有基于手机号的查找场景(比如查“手机号等于 13888888888 的用户”)都是通过这个字段完成的,而不是查密文。 「phone_prefix / phone_last4」 这两个是我们保留下来的明文段,用于支持一些常见的模糊查找。 比如客服说:“能不能查一下尾号是 5678 的用户?”或者运营要查“138 开头的手机号用户”,再比如我们要导出所有 138 开头的用户手机号给市场部门做短信通知,这类场景我们就通过这两个字段来查。 我们明确只保留前缀和后缀两个可查段,不支持任意位置模糊,比如 「key_version」 密钥是一定要支持轮换的,这个字段就是用来记录每条数据使用的是哪一版密钥。以后密钥升级时,不需要一次性重加密所有数据,而是根据版本去解密、或后台分批升级。 姓名字段的加密设计手机号搞定之后,接下来就是姓名字段的处理。相比手机号,姓名的使用频率没那么高,基本都是用于后台展示,或者做一些精确匹配的筛选,比如查“名字叫张三”的用户。 我们这边对姓名的使用场景也比较明确:
基于这些情况,我们的加密策略就比较简单了:
字段设计跟手机号差不多,也是一组密文字段 + 一个索引字段。
我们没有保留什么前缀、后缀字段,也没有拆成字或词来支持模糊查询,这一块我们定得很死:「姓名只支持等值查,不能模糊匹配」。 主要原因也是出于安全考虑——中文姓名空间太小了,如果支持模糊,风险太大,查个“李”就能把一堆用户暴露出来。所以我们业务上直接禁止这类需求,也就没必要为它设计复杂的字段结构。 身份证字段的加密设计身份证号这块,我们在处理时是比较谨慎的。一方面它是非常敏感的个人信息,属于法律明确定义的“个人敏感信息”;另一方面,它本身就包含了大量可识别的数据,比如出生日期、性别、地区编码这些,都能被人反推出来。 我们这边业务对身份证的使用需求,主要集中在以下几个场景:
根据这些场景,我们做了如下设计:
字段思路基本和手机号一致:密文存储 + 索引查找 + 结构化派生字段。 我们不会在业务里去“模糊查身份证”,但为了支持按出生年月、性别这些维度做筛选,我们在应用层「解析身份证后把这些派生字段存入库中」,这样就可以支持场景需求,又不需要对加密字段本身做任何操作。 下面是我们几个典型的查询示例: 精确查某个身份证号(等值查询)
这个查询是通过应用层先算出身份证号的 HMAC 值,然后用 筛选出生在 1990 年的用户
查询性别为女性的用户
查询地区编码为 310101 的用户(例如上海黄浦)
这些字段都是在实名认证通过时就一次性提取并存入数据库的,系统运行过程中不需要每次都重新解析身份证号。 我们也没有保留身份证号的前缀、后缀等字段,更不支持任何形式的 邮箱字段的加密设计邮箱这个字段,在我们系统里的使用频率相对没那么高,主要出现在这些地方:
和前面的手机号、身份证一样,我们也对邮箱做了加密处理,但整体设计更轻量一些:
思路还是一样的:「加密存原文,保留索引,适当结构拆分」。 email_cipher / iv / tag和其他字段一样, email_idx邮箱虽然不像手机号那样高频查询,但也有等值查的场景,比如用户忘记绑定的手机号、用邮箱登录的时候,就会用到这个字段。我们这里同样是通过 HMAC 算法生成一个不可逆的索引值,用于精准查找。
应用层先把用户输入的邮箱标准化(比如统一小写、去空格),再计算 HMAC,和数据库里的 email_domain这个字段是我们主动拆出来的,用来支持一些按邮箱域名的筛选,比如:
这个字段是通过应用层在用户绑定邮箱时就拆出来的,直接存明文。我们系统默认所有邮箱都在存储前做了统一处理,比如去空格、小写化,这样可以保证查询时不会出错。 说明一下我们没做什么我们没有做“邮箱前缀模糊查”,比如查 后台展示这么多字段,不会查起来很麻烦吗?在我们把用户敏感信息都做了加密拆分之后,一个最直接的问题就摆在面前了: 后台列表要展示用户信息,手机号、邮箱、身份证、姓名这些都加密了,字段又不在主表里,是不是每次都要联合三四张表?这样查性能压力大不说,写代码也麻烦。 我们一开始也试过用视图、用聚合查询,甚至做过服务端拼接,但最终都觉得太重。 后来我们选择了一个「最简单、最实用的做法」: ❝ 我们是怎么做的?我们在用户主表里增加了下面几个字段:
这些字段的生成方式非常简单,都是在用户注册或实名认证成功后,系统在后端加密字段的同时,同步生成一份脱敏展示内容,写入用户主表。 举个例子:
这些脱敏字段用在哪里?我们限定这些字段「只用于展示场景」,比如:
这些字段「不会参与任何查询、筛选或逻辑判断」,只作为纯展示字段存在,避免每次都去解密原文、拼接脱敏字符串。 为什么要这么做?一方面是出于性能考虑。加密字段都拆出去了,如果展示时还要联合三四个表、还要解密、还要拼接脱敏格式,展示一个列表可能就要跑几十次逻辑,根本扛不住。 另一方面也是代码维护成本的问题。每个功能点都写一套“解密 + 脱敏”的逻辑,容易出错不说,还到处复制粘贴,不好统一。 我们统一用这个做法之后:
脱敏字段的写入时机?统一都是在用户信息首次写入或者更新敏感字段时自动写入。 比如:
我们在内部封装了一套写入逻辑,所有调用加密写入的地方,都会自动带上脱敏字段生成。 这套加密方案落地后,我们遇到的一些现实问题上面说的这一整套加密设计,基本能覆盖大多数合规要求和常见业务场景了。 1. 密钥怎么管理是个麻烦事刚开始大家说得好好的“支持密钥轮换”,但真要搞,才发现没几个人愿意接这个锅。 我们一开始是直接把 key 写在配置文件里的,然后放到环境变量。这个方式其实风险挺大的,密钥如果泄露,数据库里的敏感信息几乎就是裸奔。 后来我们简单封装了一下,把加密解密逻辑统一放到一个工具类里,预留了切换密钥的能力。再后来才慢慢开始接入 KMS。 「建议:」 如果团队能力允许,尽早把 key 管理这块独立出来,别写死在代码里。 2. 有人想查中间模糊,办不到我们只支持手机号前缀、后缀,身份证出生年月,邮箱域名。 总之我们明确限制搜索能力是出于安全考虑,不支持就是不支持,当时就统一回:“不能查,这是设计上就不允许的。” 「建议:」 上线之前就把查询能力边界定清楚,不支持模糊就别让前端传 3. 解密逻辑容易被滥用一开始我们把解密逻辑封装成公共函数,想着大家统一调用。但上线之后,慢慢发现有些地方开始滥用了: 这玩意一旦用得太随意,加密就等于白做了。 「建议:」 所有解密操作都要有明确入口,最好加审计。不是谁想解就解。我们后来是把“解密行为”统一放在一个 service 里,并做了日志记录。 4. 历史数据加密是个大坑上线之前的老数据全是明文的,怎么办?总不能一刀切直接换表吧。 我们是做了一个“回填脚本”,分批把旧数据慢慢加密进来,跑了两周才完全处理完。中间还遇到过字符编码、无效数据的问题,有些字段根本没值。 「建议:」 上线前要考虑数据迁移路径。尽量用“新增字段 + 回填 + 双写”方式做平滑迁移,别想着一次到位。 5. BI、分析、导出系统全挂了敏感字段一加密,BI 查询直接废了。原来可以查手机号前 3 位注册分布,现在什么也查不了。 后来我们给 BI 系统专门开了一张“脱敏副本表”,只保留能分析的那几段,比如手机号段、邮箱域名、出生年月等。 「建议:」 做加密前,先跟 BI 团队聊一聊,把他们日常用到的字段结构先梳理出来。该保留结构的保留,但不能留下泄露口子。 6. 后台导出要啥都给,这也很危险我们系统里有后台导出功能。以前是全字段导出,连身份证号、手机号全都明文给到 Excel。 上线加密后,我们调整了导出规则:
「建议:」 导出、审计这类场景最容易出问题,别放松,要么不给,要么严格审批。 总的来说这一套加密方案本身没问题,关键是我们上线之后怎么落地、怎么控制使用。 我们最终要保证的是: ❝ 我们选的不是最完美,但是真的够用了说实话,我们在做这个方案之前,也看了很多网上关于数据库加密的文章,不少都讲得很完整,也很专业,确实值得学习。 但我们没有直接照搬任何一种方案。原因很简单: ❝ 我们不是在做一个“为了合规而合规”的系统,也不是为了安全做而做。 所以最后这套方案看起来可能没那么“高级”,也不通用,但它「足够简单、够安全、能落地」,而且我们确实在上线之后运行得还不错。 如果我们项目里也刚好面临类似的问题,我觉得不一定要一上来就追求通用框架或者最全场景支持,不如从业务出发,做一套「适合我们自己系统的加密方案」,先跑起来,再逐步优化,也完全来得及。 阅读原文:https://mp.weixin.qq.com/s/2LNJkaFnClIrTaKL6UW8Sg 该文章在 2025/10/10 15:01:45 编辑过 |
关键字查询
相关文章
正在查询... |