简介:这份资源面向 Oracle 数据库开发与运维人员,提供一套自定义加密解密函数,用于解决敏感数据脱敏、加密存储与合规传输问题。包内共 3 个文件,以 2 个 sql 脚本和 1 个 txt 说明为主,压缩包约 5KB,其中 sql 文件分别实现加密与解密逻辑,txt 文档给出使用说明与注释,便于快速集成到现有库中。核心函数 ENCRYPT_DES 与 DECRYPT_DES 基于 DES 标准,参数可配置,支持按需调整密钥与数据长度,兼顾加密强度与灵活性,适用于金融账户信息、医疗患者隐私、电商身份与权限数据等场景。代码附带详尽注释,降低理解与维护成本,经过测试优化后稳定性有保障。目前已有 521 人学习下载,适合需要落地数据安全合规方案的中高级开发者参考复用。
1. 为什么我宁愿手写 Oracle 加密函数,也不直接调 DBMS_CRYPTO
去年做等保整改,有个老库要过合规检查,身份证和手机号全是明文躺在 VARCHAR2 字段里。第一反应是用 DBMS_CRYPTO,结果发现这玩意儿在 10g 上根本没有,11g 还得额外授权,生产库 DBA 死活不给 EXECUTE 权限。折腾两天后我换了个思路:用 Oracle 自带的 UTL_ENCODE 和 DBMS_OBFUSCATION_TOOLKIT 手搓一套自定义加密解密函数,不依赖任何外部包,普通开发账号就能跑。
这套方案的核心就三件事:加密存储、数据脱敏、合规可审计。适合两类人——一是像我这样在老旧 Oracle 环境里做等保整改、拿不到高级权限的;二是需要在 SQL 层直接对敏感字段做加解密、不想把逻辑放到应用层的。下面把我踩过的坑和能直接抄的代码全倒出来。
2. 自定义加密函数怎么选型:从 DBMS_OBFUSCATION_TOOLKIT 到 UTL_ENCODE
2.1 为什么不用 DBMS_CRYPTO
先说清楚选型逻辑。DBMS_CRYPTO 是 Oracle 10g R2 之后才有的包,支持 AES、DES、3DES 等标准算法,功能确实强。但实际落地时有三个硬伤:
第一,权限问题。DBMS_CRYPTO 默认只给 SYS 执行权限,普通用户要调用必须由 DBA 显式授权。在很多企业里,DBA 和应用开发是两个部门,走一次授权流程少则三天多则一周。等保整改往往有 deadline,等不起。
第二,版本兼容。我手上有个 10.2.0.4 的老库,DBMS_CRYPTO 压根不存在。升级数据库?业务方直接说不可能。这种情况下只能用 DBMS_OBFUSCATION_TOOLKIT,这个包从 8i 就有了,兼容性拉满。
第三,审计要求。等保 2.0 里对加密算法有明确要求,但没规定必须用某个特定包。只要加密逻辑可审计、密钥管理有流程、解密有权限控制,自定义函数完全能过。我后来把加密函数源码打印出来给测评机构看,对方确认逻辑没问题就过了。
所以选型结论很明确:能用 DBMS_CRYPTO 就用,用不了就上 DBMS_OBFUSCATION_TOOLKIT + UTL_ENCODE 组合。下面重点讲后者。
2.2 核心函数拆解:DES3 加密 + Base64 编码
DBMS_OBFUSCATION_TOOLKIT 提供的 DES3_ENCRYPT 函数,输入是 RAW 类型,输出也是 RAW。但我们的字段是 VARCHAR2,直接存 RAW 会乱码。所以中间要加一层 UTL_ENCODE.BASE64_ENCODE 做编码转换。
整个链路是这样的:
明文 VARCHAR2 → UTL_I18N.STRING_TO_RAW 转 RAW → DES3_ENCRYPT 加密 → UTL_ENCODE.BASE64_ENCODE 编码 → 密文 VARCHAR2 存库解密反过来:
密文 VARCHAR2 → UTL_ENCODE.BASE64_DECODE 解码 → DES3_DECRYPT 解密 → UTL_I18N.RAW_TO_CHAR 转字符串 → 明文 VARCHAR2这里有个关键点:DES3_ENCRYPT 的密钥必须是 16 或 24 字节。我一般用 24 字节,安全性更高。密钥不能硬编码在函数里,常见做法是存到一张单独的密钥表,加访问控制,或者通过 SYS_CONTEXT 从应用传入。
2.3 建包建函数:完整可执行脚本
先建一个加密包,把加解密和密钥管理都封进去:
-- 创建加密包规范 CREATE OR REPLACE PACKAGE pkg_crypto AS -- 加密函数:输入明文,返回Base64编码的密文 FUNCTION encrypt_data(p_plain_text IN VARCHAR2) RETURN VARCHAR2; -- 解密函数:输入Base64密文,返回明文 FUNCTION decrypt_data(p_cipher_text IN VARCHAR2) RETURN VARCHAR2; -- 脱敏函数:保留前n位和后m位,中间用*代替 FUNCTION mask_data(p_input IN VARCHAR2, p_prefix IN NUMBER, p_suffix IN NUMBER) RETURN VARCHAR2; END pkg_crypto; / -- 创建加密包体 CREATE OR REPLACE PACKAGE BODY pkg_crypto AS -- 密钥常量,实际项目中应从密钥表读取 c_key CONSTANT VARCHAR2(24) := 'MySecretKey2024!@#$%^&'; FUNCTION encrypt_data(p_plain_text IN VARCHAR2) RETURN VARCHAR2 IS v_raw RAW(2000); v_encrypted RAW(2000); v_result VARCHAR2(4000); BEGIN -- 空值直接返回 IF p_plain_text IS NULL THEN RETURN NULL; END IF; -- 字符串转RAW v_raw := UTL_I18N.STRING_TO_RAW(p_plain_text, 'AL32UTF8'); -- DES3加密 DBMS_OBFUSCATION_TOOLKIT.DES3_ENCRYPT( input_string => v_raw, key_string => UTL_I18N.STRING_TO_RAW(c_key, 'AL32UTF8'), encrypted_string => v_encrypted ); -- Base64编码 v_result := UTL_ENCODE.BASE64_ENCODE(v_encrypted); RETURN v_result; EXCEPTION WHEN OTHERS THEN -- 记录日志后抛出 RAISE_APPLICATION_ERROR(-20001, '加密失败: ' || SQLERRM); END encrypt_data; FUNCTION decrypt_data(p_cipher_text IN VARCHAR2) RETURN VARCHAR2 IS v_decoded RAW(2000); v_decrypted RAW(2000); v_result VARCHAR2(4000); BEGIN IF p_cipher_text IS NULL THEN RETURN NULL; END IF; -- Base64解码 v_decoded := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(p_cipher_text)); -- DES3解密 DBMS_OBFUSCATION_TOOLKIT.DES3_DECRYPT( input_string => v_decoded, key_string => UTL_I18N.STRING_TO_RAW(c_key, 'AL32UTF8'), decrypted_string => v_decrypted ); -- RAW转字符串 v_result := UTL_I18N.RAW_TO_CHAR(v_decrypted, 'AL32UTF8'); RETURN v_result; EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20002, '解密失败: ' || SQLERRM); END decrypt_data; FUNCTION mask_data(p_input IN VARCHAR2, p_prefix IN NUMBER, p_suffix IN NUMBER) RETURN VARCHAR2 IS v_len NUMBER; v_mask VARCHAR2(100); v_result VARCHAR2(4000); BEGIN IF p_input IS NULL THEN RETURN NULL; END IF; v_len := LENGTH(p_input); -- 长度不够直接全脱敏 IF v_len <= p_prefix + p_suffix THEN RETURN RPAD('*', v_len, '*'); END IF; -- 构造中间掩码 v_mask := RPAD('*', v_len - p_prefix - p_suffix, '*'); v_result := SUBSTR(p_input, 1, p_prefix) || v_mask || SUBSTR(p_input, -p_suffix); RETURN v_result; END mask_data; END pkg_crypto; /这段代码有三个关键设计点。第一,密钥用 CONSTANT 定义在包体里,实际项目应该改成从独立密钥表查询,并且给密钥表加单独的访问控制。第二,异常处理里用 RAISE_APPLICATION_ERROR 抛出自定义错误码,方便应用层区分是加密失败还是解密失败。第三,脱敏函数支持动态指定前后保留位数,手机号可以保留前3后4,身份证保留前6后4。
2.4 测试验证:加密解密跑一遍
建完包先别急着改生产数据,拿测试表跑一遍:
-- 建测试表 CREATE TABLE t_user_sensitive ( id NUMBER PRIMARY KEY, user_name VARCHAR2(50), id_card_enc VARCHAR2(200), -- 加密后的身份证 phone_enc VARCHAR2(200), -- 加密后的手机号 id_card_mask VARCHAR2(50), -- 脱敏后的身份证 phone_mask VARCHAR2(50) -- 脱敏后的手机号 ); -- 插入测试数据 INSERT INTO t_user_sensitive (id, user_name, id_card_enc, phone_enc, id_card_mask, phone_mask) VALUES ( 1, '张三', pkg_crypto.encrypt_data('110101199001011234'), pkg_crypto.encrypt_data('13800138000'), pkg_crypto.mask_data('110101199001011234', 6, 4), pkg_crypto.mask_data('13800138000', 3, 4) ); COMMIT; -- 验证:查加密数据 SELECT id, user_name, id_card_enc, phone_enc FROM t_user_sensitive WHERE id = 1; -- 验证:解密还原 SELECT id, user_name, pkg_crypto.decrypt_data(id_card_enc) AS id_card_plain, pkg_crypto.decrypt_data(phone_enc) AS phone_plain, id_card_mask, phone_mask FROM t_user_sensitive WHERE id = 1;跑完应该看到:id_card_enc 是一串 Base64 乱码,id_card_plain 还原成 110101199001011234,id_card_mask 显示 110101********1234。如果解密出来是乱码,八成是字符集问题,检查 STRING_TO_RAW 和 RAW_TO_CHAR 的字符集参数是否一致。
3. 存量数据怎么平滑迁移:分批加密 + 双写过渡
3.1 迁移策略:先加列再回填
生产库不可能停服让你慢慢加密。我一般分四步走:
第一步,给敏感字段加加密列。比如原表有 id_card 字段,新增 id_card_enc 字段。
第二步,写一个存储过程分批回填。每次处理 5000 行,避免大事务把 undo 表空间撑爆。
第三步,应用层双写。新数据同时写明文列和加密列,读的时候优先读加密列。
第四步,验证无误后,把明文列清空或改名为备份列。
回填存储过程大概长这样:
CREATE OR REPLACE PROCEDURE sp_migrate_encrypt( p_batch_size IN NUMBER DEFAULT 5000 ) IS v_total NUMBER; v_done NUMBER := 0; v_start NUMBER := 0; BEGIN SELECT COUNT(*) INTO v_total FROM t_user_sensitive WHERE id_card_enc IS NULL; WHILE v_done < v_total LOOP -- 分批更新 UPDATE t_user_sensitive SET id_card_enc = pkg_crypto.encrypt_data(id_card), phone_enc = pkg_crypto.encrypt_data(phone) WHERE id IN ( SELECT id FROM t_user_sensitive WHERE id_card_enc IS NULL AND ROWNUM <= p_batch_size ); v_done := v_done + SQL%ROWCOUNT; COMMIT; -- 记录进度 DBMS_OUTPUT.PUT_LINE('已处理: ' || v_done || '/' || v_total); -- 避免锁等待 DBMS_LOCK.SLEEP(0.5); END LOOP; DBMS_OUTPUT.PUT_LINE('迁移完成,共处理 ' || v_done || ' 条'); END sp_migrate_encrypt; /这里有几个参数要调。p_batch_size 默认 5000,如果单行数据大或者 undo 表空间小,降到 1000。DBMS_LOCK.SLEEP(0.5) 是给主库留喘息时间,生产环境建议 1 秒以上。ROWNUM 条件必须放在子查询里,直接写在 UPDATE 的 WHERE 里会导致全表扫描。
3.2 双写过渡期的查询兼容
双写期间,应用层查询要兼容新旧两种数据。常见做法是建一个视图,把加密列解密后和明文列做 COALESCE:
CREATE OR REPLACE VIEW v_user_sensitive AS SELECT id, user_name, COALESCE(pkg_crypto.decrypt_data(id_card_enc), id_card) AS id_card, COALESCE(pkg_crypto.decrypt_data(phone_enc), phone) AS phone, id_card_mask, phone_mask FROM t_user_sensitive;这样应用层不用改代码,直接查视图就行。等所有数据都迁移完,再把视图改成只读加密列。
3.3 性能影响实测
加密解密是有 CPU 开销的。我在测试库上跑过对比:10 万行数据,全表扫描解密比直接读明文慢 3 到 5 倍。如果查询条件里用到加密字段,比如 WHERE id_card_enc = pkg_crypto.encrypt_data('110101...'),那索引完全用不上,只能全表扫。
所以有个原则:加密字段只用于存储和展示,不要用于查询条件。需要按身份证查人,就额外存一个哈希列,用 SHA256 做索引。这个哈希列不可逆,但能精确匹配。
4. 避坑指南:密钥管理、字符集和权限的五个血泪教训
4.1 密钥硬编码在包体里,源码一泄露全完蛋
现象:开发图省事,把密钥写成 CONSTANT 放在包体里。结果代码仓库权限没管好,外包人员拿到了源码,所有加密数据等于裸奔。
原因:Oracle 的包体源码可以通过 USER_SOURCE 视图查到,只要有权限就能看。
解决:密钥必须外置。建一张密钥表,加独立表空间和访问控制,包体里通过函数动态获取。更严格的做法是用 Oracle Wallet,但配置复杂,一般项目用密钥表就够了。
-- 密钥表 CREATE TABLE t_crypto_key ( key_id NUMBER PRIMARY KEY, key_value VARCHAR2(100), create_time DATE DEFAULT SYSDATE, is_active NUMBER(1) DEFAULT 1 ); -- 插入密钥 INSERT INTO t_crypto_key VALUES (1, 'MySecretKey2024!@#$%^&', SYSDATE, 1); -- 包体中改为查询获取 FUNCTION get_key RETURN VARCHAR2 IS v_key VARCHAR2(100); BEGIN SELECT key_value INTO v_key FROM t_crypto_key WHERE key_id = 1 AND is_active = 1; RETURN v_key; END;4.2 字符集不一致导致解密乱码
现象:加密时用 AL32UTF8,解密时用 ZHS16GBK,出来的明文是问号或者乱码。
原因:STRING_TO_RAW 和 RAW_TO_CHAR 的字符集参数必须严格一致,否则字节流对不上。
解决:统一用 AL32UTF8,这是 Oracle 推荐的字符集。如果数据库本身是 ZHS16GBK,那加密解密都用 ZHS16GBK,别混着来。可以在包体里定义一个常量字符集,所有函数引用同一个常量。
4.3 加密后字段长度不够,数据被截断
现象:加密前手机号 11 位,加密后 Base64 字符串变成 40 多位,原字段 VARCHAR2(20) 直接报错或者截断。
原因:DES3 加密后数据膨胀,Base64 编码又增加约 33% 长度。
解决:加密列的长度至少是明文的 4 倍。手机号 11 位,加密列给 VARCHAR2(100)。身份证 18 位,给 VARCHAR2(200)。建表时宁大勿小,VARCHAR2(4000) 也不占实际存储空间。
4.4 普通用户没有 DBMS_OBFUSCATION_TOOLKIT 权限
现象:调用加密函数报 ORA-06550 或 PLS-00201,提示标识符必须声明。
原因:DBMS_OBFUSCATION_TOOLKIT 默认只给 SYS 执行权限。
解决:让 DBA 授权,或者用 SYS 建一个公共同义词。如果 DBA 不配合,还有个偏方:用 UTL_ENCODE 加自定义异或算法,纯 SQL 实现,不需要任何特殊包。但安全性差很多,只适合对加密强度要求不高的场景。
-- 授权语句,需要DBA执行 GRANT EXECUTE ON DBMS_OBFUSCATION_TOOLKIT TO your_user; GRANT EXECUTE ON UTL_ENCODE TO your_user; GRANT EXECUTE ON UTL_I18N TO your_user;4.5 批量解密时 PGA 内存溢出
现象:一次性解密几十万行数据,报 ORA-04030 out of process memory。
原因:每次解密都在 PGA 里分配 RAW 变量,批量操作时累积占用过大。
解决:分批处理,每批 1000 到 5000 行,处理完显式 COMMIT 释放资源。如果还不行,调大 PGA_AGGREGATE_TARGET 参数,或者改用游标逐行处理。
5. 进阶技巧:用哈希列做等值查询,兼顾安全与性能
加密字段没法建索引,这是硬伤。但业务上又经常需要按身份证号精确查询,怎么办?我的做法是加一个哈希列,存 SHA256 值,在这个列上建索引。
-- 加哈希列 ALTER TABLE t_user_sensitive ADD id_card_hash VARCHAR2(64); -- 更新哈希值 UPDATE t_user_sensitive SET id_card_hash = STANDARD_HASH(id_card, 'SHA256'); -- 建索引 CREATE INDEX idx_id_card_hash ON t_user_sensitive(id_card_hash); -- 查询时先算哈希再匹配 SELECT * FROM t_user_sensitive WHERE id_card_hash = STANDARD_HASH('110101199001011234', 'SHA256');STANDARD_HASH 是 Oracle 12c 才有的函数,11g 及以下用 DBMS_CRYPTO.HASH 或者自定义哈希。哈希列不可逆,即使泄露也无法还原明文,安全性比加密列还高。但要注意加盐,防止彩虹表攻击。加盐的做法是在哈希前拼一个固定字符串:
UPDATE t_user_sensitive SET id_card_hash = STANDARD_HASH('SALT_' || id_card, 'SHA256');盐值同样要外置管理,不能硬编码。
验证方法很简单:拿一条已知数据,手动算哈希,看是否和库里一致。如果对不上,检查盐值是否一致、字符集是否一致。
还有个技巧是脱敏和加密配合使用。对外展示的界面查脱敏列,内部业务系统查解密列,审计日志只记录哈希列。这样即使日志泄露,也拿不到明文。
从那以后我每次做加密迁移,都强制走一遍「测试库全量验证 → 生产库分批回填 → 双写过渡 → 明文列归档」的流程,再急也不跳过测试库那步。希望帮到你。
本文还有配套的精品资源,点击获取