SQL改写基础:INSERT/UPDATE/SELECT中的加解密实战
好,咱们进入正题。我记得刚接手第一个加密改造项目时,甲方只给了一句话:“把这几张表的敏感字段都加密了,业务不能停。”说白了,列级加密的核心就两条:写入时加密,读取时解密。但怎么写得顺手、读得高效,这里头有不少门道。
4.1 加密函数的选择与准备
在实际项目中,我选加密函数有几个硬指标:性能不能太差、密钥管理要方便、数据库要原生支持。你想想看,如果每写一条记录都卡几秒,业务部门肯定不答应。
我个人习惯用AES-256,它平衡了安全性和性能。不同数据库的调用方式略有差异:
| 数据库 | 加密函数 | 解密函数 |
|---|---|---|
| MySQL | AES_ENCRYPT() | AES_DECRYPT() |
| PostgreSQL | pgp_sym_encrypt() | pgp_sym_decrypt() |
| Oracle | DBMS_CRYPTO.ENCRYPT | DBMS_CRYPTO.DECRYPT |
| SQL Server | ENCRYPTBYKEY() | DECRYPTBYKEY() |
4.2 INSERT语句中的加密改写
这是最基础的场景。假设我们有一张用户表 t_user,其中 phone 字段需要加密存储。原来的INSERT长这样:
-- 原始SQL
INSERT INTO t_user (user_id, user_name, phone)
VALUES (1001, '张三', '13800138000');
-- 改写后SQL
INSERT INTO t_user (user_id, user_name, phone)
VALUES (1001, '张三', AES_ENCRYPT('13800138000', 'my_secret_key'));
嗯,这里要注意:加密后的数据是二进制类型。如果你原来字段定义是 VARCHAR(20),直接就炸了。我遇到过一位同事,改完INSERT后一直报错,排查了半天才发现是字段类型没改。
正确的表结构应该是:
CREATE TABLE t_user (
user_id INT PRIMARY KEY,
user_name VARCHAR(50),
phone VARBINARY(256) -- 改成二进制类型,长度留余量
);
4.3 UPDATE语句中的加密改写
UPDATE的逻辑和INSERT类似,但有一个坑——部分更新。有些业务场景只需要更新某几个加密字段,这时候就要注意了:
-- 只更新加密字段,其他字段不变
UPDATE t_user
SET phone = AES_ENCRYPT('13900139000', 'my_secret_key')
WHERE user_id = 1001;
-- 多个加密字段一起更新
UPDATE t_user
SET
phone = AES_ENCRYPT('13900139000', 'my_secret_key'),
email = AES_ENCRYPT('zhangsan@example.com', 'my_secret_key')
WHERE user_id = 1001;
你想想看,如果更新时顺手把别的字段也覆盖了,那后果……所以写UPDATE时,WHERE条件一定要带对。我习惯在UPDATE前先查一次,确认要改哪几条记录。
4.4 SELECT语句中的解密还原
加密存进去了,怎么读出来呢?说白了就是反向操作:
-- 原始SQL
SELECT user_id, user_name, phone FROM t_user WHERE user_id = 1001;
-- 改写后SQL
SELECT
user_id,
user_name,
AES_DECRYPT(phone, 'my_secret_key') AS phone
FROM t_user
WHERE user_id = 1001;
这里有个关键点:AES_DECRYPT返回的也是二进制,需要转成字符串。MySQL里可以套一层 CAST:
SELECT
user_id,
user_name,
CAST(AES_DECRYPT(phone, 'my_secret_key') AS CHAR) AS phone
FROM t_user;
4.5 实战中的常见场景
光讲CRUD太干了,我结合一个真实案例说说。去年做个电商订单系统,订单表有十几个字段需要加密,包括收货人、手机号、地址。最头疼的是批量插入:
-- 批量插入加密数据
INSERT INTO t_order (order_id, user_id, receiver_name, receiver_phone, receiver_addr, order_amount)
VALUES
(2024001, 1001, AES_ENCRYPT('李四', 'key'), AES_ENCRYPT('13600136000', 'key'), AES_ENCRYPT('北京市朝阳区xxx', 'key'), 199.00),
(2024002, 1002, AES_ENCRYPT('王五', 'key'), AES_ENCRYPT('13700137000', 'key'), AES_ENCRYPT('上海市浦东新区xxx', 'key'), 299.00),
(2024003, 1003, AES_ENCRYPT('赵六', 'key'), AES_ENCRYPT('13800138000', 'key'), AES_ENCRYPT('广州市天河区xxx', 'key'), 399.00);
这种做法性能还行,但有个问题:每个值都重复传密钥。如果要用不同密钥加密不同字段怎么办?可以用存储过程或应用层封装一下。
4.6 最容易翻车的点:WHERE条件
我曾经遇到一个线上事故,某同事在WHERE里直接写解密后的值:
-- 错误写法!索引失效
SELECT * FROM t_user
WHERE CAST(AES_DECRYPT(phone, 'key') AS CHAR) = '13800138000';
为什么说这是错的?因为全表扫描。数据库拿到每条数据都要先解密,再比较,几十万条记录直接就慢查询了。而且解密后的值没法建索引。
正确的做法是:先用其他条件缩小范围,或者在应用层做查询。如果非要按加密字段查询,可以考虑加一个哈希索引列,存明文SHA256的摘要,用来做精确匹配。这个后面章节我们会细聊。
4.7 知识体系总览
到这里,INSERT/UPDATE/SELECT的加密改写基本讲完了。我画了张图,把这个流程串起来:
4.8 核心要点总结
为了让你记得更牢,我把今天的内容整理成一个清单:
- INSERT:在VALUES中用加密函数替换明文值,字段类型要改成VARBINARY
- UPDATE:在SET子句中调用加密函数,注意不要误覆盖其他字段
- SELECT:用解密函数还原数据,记得用CAST转成字符串
- 禁止:在WHERE条件中使用解密函数,这是性能杀手
- 密钥:统一管理,别硬编码。改一次密钥你就知道疼了