☰
Oracle自定义加密解密函数实战:从DBMS_CRYPTO封装到数据脱敏与性能优化
2026/10/11 16:01:12 网站建设 项目流程

简介:这份资源面向 Oracle 数据库开发与运维人员,提供一套自定义加密解密函数,用于解决敏感数据脱敏、加密存储与合规传输问题。包内共 3 个文件,以 2 个 sql 脚本和 1 个 txt 说明文档为主,压缩包约 5KB,sql 脚本分别对应加密与解密逻辑,txt 文档则承担使用说明与注释补充,结构精简、便于直接导入数据库使用。资源核心为 ENCRYPT_DES 与 DECRYPT_DES 两个函数,基于 DES 加密标准,参数可灵活配置密钥与数据长度,适用于本地部署及云数据库环境,可覆盖金融账户信息、医疗患者隐私、电商用户身份与权限数据等场景。代码附带详尽注释,便于快速理解与二次维护,降低加密解密流程的接入成本。目前已有 521 人学习下载,适合需要落地数据安全合规方案的中高级开发者参考。

1. 为什么在 Oracle 里自己写加解密函数,而不是直接调 DBMS_CRYPTO

很多团队第一次做数据安全合规,第一反应是开 Oracle 自带的DBMS_CRYPTO包,觉得官方的东西肯定够用。我在一个做订单系统的项目里也这么想过,直到等保测评要求「敏感字段落库必须密文、密钥不能和数据库同机存放、脱敏展示要能按角色区分」,才发现光靠一个包解决不了全部问题。DBMS_CRYPTO提供的是 AES、DES、哈希这些底层原语,它不负责你的密钥怎么管、字段怎么选、脱敏规则怎么配、历史数据怎么平滑迁移。真正落地时,你需要的是「自定义加密解密函数」这一层封装:把密钥来源、加密模式、编码方式、脱敏策略全部收口到几个 PL/SQL 函数里,业务侧只调f_encrypt和f_decrypt,不关心底层细节。

这篇笔记讲的就是这套东西怎么从零搭起来:函数怎么写、密钥怎么放、性能怎么扛、脱敏和加密怎么配合、上线时哪些坑会让你半夜被叫起来。适合正在做 Oracle 数据安全合规、数据脱敏、加密存储的 DBA 和后端开发,尤其是那些被测评报告追着跑、又不想把业务代码改得面目全非的人。下面按「先能跑通、再谈合规、最后谈性能」的顺序推。

2. 自定义加密解密函数的选型与最小可运行实现

2.1 为什么不用 DBMS_CRYPTO 裸调,而要再包一层

裸调DBMS_CRYPTO.ENCRYPT的问题在于参数太散。每次调用你都要传加密算法、链模式、填充方式、密钥、IV,业务代码里散落一堆常量,改一次密钥要全库搜。更麻烦的是密钥来源:如果密钥硬编码在存储过程里,等于没加密;如果放在应用层传进来,那数据库审计日志里可能留下明文密钥。自定义函数的价值就是把「算法参数 + 密钥获取 + 编码转换」三件事封死在一个地方,业务侧只传明文和业务标识。

常见做法是建一个独立的 schema,比如SEC_CRYPTO,里面放函数和一张密钥配置表。密钥配置表本身也要保护,通常只给函数属主读权限,其他用户通过EXECUTE授权调用函数,看不到表。这样即使有人拿到业务账号,也拿不到密钥。

2.2 最小可运行的 AES 加解密函数

先给一个能直接跑的版本,基于DBMS_CRYPTO的 AES-128-CBC。密钥从配置表读,IV 每次随机生成并拼在密文前面,这样同一个明文每次加密结果不同,避免模式泄露。

-- 密钥配置表,放在 SEC_CRYPTO schema 下 CREATE TABLE SEC_CRYPTO.T_KEY_STORE ( KEY_ID VARCHAR2(32) PRIMARY KEY, KEY_HEX VARCHAR2(64) NOT NULL, -- 32位十六进制,对应16字节AES-128密钥 CREATE_TIME DATE DEFAULT SYSDATE, IS_ACTIVE CHAR(1) DEFAULT 'Y' ); -- 插入一条测试密钥,生产环境用随机生成工具产生,不要用这个 INSERT INTO SEC_CRYPTO.T_KEY_STORE (KEY_ID, KEY_HEX) VALUES ('ORDER_KEY', '0123456789ABCDEF0123456789ABCDEF'); COMMIT; -- 加密函数:返回 十六进制(IV) + 十六进制(密文) CREATE OR REPLACE FUNCTION SEC_CRYPTO.F_ENCRYPT( P_PLAIN IN VARCHAR2, P_KEY_ID IN VARCHAR2 ) RETURN VARCHAR2 IS V_KEY_RAW RAW(16); V_IV_RAW RAW(16); V_ENC_RAW RAW(32767); V_KEY_HEX VARCHAR2(64); BEGIN -- 取密钥 SELECT KEY_HEX INTO V_KEY_HEX FROM SEC_CRYPTO.T_KEY_STORE WHERE KEY_ID = P_KEY_ID AND IS_ACTIVE = 'Y'; V_KEY_RAW := HEXTORAW(V_KEY_HEX); -- 生成随机 IV V_IV_RAW := DBMS_CRYPTO.RANDOMBYTES(16); -- AES-128-CBC + PKCS5 填充 V_ENC_RAW := DBMS_CRYPTO.ENCRYPT( src => UTL_I18N.STRING_TO_RAW(P_PLAIN, 'AL32UTF8'), typ => DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5, key => V_KEY_RAW, iv => V_IV_RAW ); RETURN RAWTOHEX(V_IV_RAW) || RAWTOHEX(V_ENC_RAW); EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20001, '密钥不存在或已停用: ' || P_KEY_ID); END; /

逻辑说明:DBMS_CRYPTO.RANDOMBYTES(16)生成 16 字节随机 IV,每次调用都不同,这是 CBC 模式的安全前提。UTL_I18N.STRING_TO_RAW把 VARCHAR2 按 UTF8 转成 RAW,避免中文乱码。返回时把 IV 的十六进制拼在密文前面,解密时先截前 32 个字符还原 IV。参数P_KEY_ID让不同业务用不同密钥,比如订单和用户表分开,降低单密钥泄露的影响面。

解密函数对应写:

CREATE OR REPLACE FUNCTION SEC_CRYPTO.F_DECRYPT( P_CIPHER IN VARCHAR2, P_KEY_ID IN VARCHAR2 ) RETURN VARCHAR2 IS V_KEY_RAW RAW(16); V_IV_RAW RAW(16); V_ENC_RAW RAW(32767); V_KEY_HEX VARCHAR2(64); V_PLAIN_RAW RAW(32767); BEGIN SELECT KEY_HEX INTO V_KEY_HEX FROM SEC_CRYPTO.T_KEY_STORE WHERE KEY_ID = P_KEY_ID AND IS_ACTIVE = 'Y'; V_KEY_RAW := HEXTORAW(V_KEY_HEX); -- 前32位是IV,后面是密文 V_IV_RAW := HEXTORAW(SUBSTR(P_CIPHER, 1, 32)); V_ENC_RAW := HEXTORAW(SUBSTR(P_CIPHER, 33)); V_PLAIN_RAW := DBMS_CRYPTO.DECRYPT( src => V_ENC_RAW, typ => DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5, key => V_KEY_RAW, iv => V_IV_RAW ); RETURN UTL_I18N.RAW_TO_CHAR(V_PLAIN_RAW, 'AL32UTF8'); EXCEPTION WHEN OTHERS THEN -- 解密失败不要抛原始错误,避免泄露信息 RAISE_APPLICATION_ERROR(-20002, '解密失败,请检查密文或密钥'); END; /

参数说明:P_CIPHER必须是F_ENCRYPT的输出格式,长度至少 33 个字符(32 位 IV + 至少 1 字节密文)。如果密文被截断或密钥不匹配,DBMS_CRYPTO.DECRYPT会抛 ORA-28817 之类的错误,这里统一转成自定义错误码,避免把底层细节暴露给调用方。

2.3 授权与调用方式

函数建好后,业务账号需要EXECUTE权限:

GRANT EXECUTE ON SEC_CRYPTO.F_ENCRYPT TO APP_USER; GRANT EXECUTE ON SEC_CRYPTO.F_DECRYPT TO APP_USER;

业务侧写入时:

INSERT INTO APP_USER.T_ORDER (ORDER_ID, CUSTOMER_NAME, ID_CARD) VALUES (1001, '张三', SEC_CRYPTO.F_ENCRYPT('110101199001011234', 'ORDER_KEY'));

查询解密:

SELECT ORDER_ID, SEC_CRYPTO.F_DECRYPT(CUSTOMER_NAME, 'ORDER_KEY') AS CUSTOMER_NAME, SEC_CRYPTO.F_DECRYPT(ID_CARD, 'ORDER_KEY') AS ID_CARD FROM APP_USER.T_ORDER WHERE ORDER_ID = 1001;

注意:F_DECRYPT放在 WHERE 条件里会导致全表扫描,因为函数调用无法走索引。如果必须按加密字段查询,常见做法是额外存一列哈希值(比如STANDARD_HASH(明文, 'SHA256'))用于等值匹配,密文列只用于展示和解密。

3. 数据脱敏与加密存储怎么配合:字段分级与动态脱敏函数

3.1 先给字段分级,再决定加密还是脱敏

不是所有敏感字段都要加密存储。手机号、身份证号这类需要精确查询的,加密后查询会变慢;而像地址、姓名这类只用于展示的,可以用脱敏函数在查询时动态处理,底层存明文但通过视图或函数控制输出。我一般按三档分:

级别典型字段存储方式查询方式
L1 高敏身份证、银行卡加密存储哈希列等值查,密文解密展示
L2 中敏手机号、邮箱加密存储同上,或部分脱敏后存明文
L3 低敏姓名、地址明文存储动态脱敏函数控制输出

分级的好处是避免「一刀切加密」带来的性能灾难。L1 字段加密后,业务侧写入和读取都要走函数,L3 字段只在展示层脱敏,对存储和索引无影响。

3.2 动态脱敏函数:按角色返回不同结果

脱敏的核心是「同一条 SQL,不同角色看到不同结果」。可以用SYS_CONTEXT取当前会话的用户名或角色,在函数里判断。

CREATE OR REPLACE FUNCTION SEC_CRYPTO.F_MASK_PHONE( P_PHONE IN VARCHAR2 ) RETURN VARCHAR2 IS V_ROLE VARCHAR2(30); BEGIN -- 取当前会话的客户端标识,实际项目可用 SYS_CONTEXT('USERENV','SESSION_USER') V_ROLE := SYS_CONTEXT('USERENV', 'SESSION_USER'); -- 管理员看全量,其他角色看脱敏 IF V_ROLE IN ('SEC_ADMIN', 'APP_USER') THEN RETURN P_PHONE; ELSE -- 保留前3后4,中间用*代替 RETURN SUBSTR(P_PHONE, 1, 3) || '****' || SUBSTR(P_PHONE, -4); END IF; END; /

逻辑说明:SYS_CONTEXT('USERENV','SESSION_USER')返回当前数据库会话用户。实际项目里更稳妥的是用应用传入的客户端标识(比如CLIENT_IDENTIFIER),因为数据库账号可能被多个应用共用。参数P_PHONE是明文手机号,函数不改变存储,只改变输出。这样底层表可以继续用明文存 L3 字段,索引不受影响。

调用时:

SELECT ORDER_ID, SEC_CRYPTO.F_MASK_PHONE(PHONE) AS PHONE_MASKED FROM APP_USER.T_ORDER;

如果字段是加密存储的,脱敏函数要套在解密之后:

SELECT SEC_CRYPTO.F_MASK_PHONE( SEC_CRYPTO.F_DECRYPT(PHONE_ENC, 'ORDER_KEY') ) AS PHONE_MASKED FROM APP_USER.T_ORDER;

注意:嵌套调用会让每行执行两次函数,大表查询时开销明显。优化方式是在应用层做脱敏,或者用物化视图预计算脱敏结果。

3.3 密钥轮换时怎么不中断业务

密钥不能一直用同一个。等保要求定期轮换,但轮换时历史数据还是用旧密钥加密的,不能直接换掉。常见做法是密钥表加版本号,加密时记录版本,解密时按版本取对应密钥。

-- 密钥表加版本列 ALTER TABLE SEC_CRYPTO.T_KEY_STORE ADD (KEY_VERSION NUMBER DEFAULT 1); -- 密文格式改为:版本号(2位) + IV(32位) + 密文 -- 加密时拼上版本号,解密时先取版本号再选密钥

这样轮换时新增数据用新版本密钥,旧数据解密时自动匹配旧版本。等旧数据全部迁移完,再把旧密钥标记停用。迁移可以用分批 UPDATE,每批几千行,避免大事务锁表。

4. 性能与兼容性避坑:那些让函数跑崩的细节

4.1 避坑一:函数调用导致索引失效,查询从毫秒变秒级

现象:对加密列做WHERE F_DECRYPT(COL,'KEY') = '张三',执行计划从 INDEX RANGE SCAN 变成 TABLE ACCESS FULL,百万行表查询从 0.1 秒涨到 8 秒。

原因:函数调用对优化器是黑盒,无法用索引。即使列上有索引,只要包了函数就用不上。

解决:额外加一列COL_HASH,存STANDARD_HASH(明文,'SHA256'),查询时用WHERE COL_HASH = STANDARD_HASH('张三','SHA256')。哈希列建索引,等值查询走索引。密文列只用于解密展示。写入时两列一起写,用触发器或应用层保证一致。

4.2 避坑二:RAW 长度超限,加密大字段报 ORA-06502

现象:加密超过 2000 字节的文本时,报ORA-06502: PL/SQL: numeric or value error。

原因:DBMS_CRYPTO.ENCRYPT返回 RAW,PL/SQL 里 RAW 最大 32767 字节,但 VARCHAR2 最大 4000 字节(标准模式)。如果明文接近 4000 字符,UTF8 编码后可能超过 4000 字节,转 RAW 再转回来就超限。

解决:加密函数返回 CLOB,或者限制单字段明文长度。更稳妥的是用DBMS_CRYPTO.ENCRYPT的 CLOB 重载版本,但那个版本在 11g 上行为不一致。我一般限制业务字段不超过 1000 字符,超长的用应用层加密。

4.3 避坑三:密钥硬编码在函数里,审计直接判不合规

现象:等保测评时,测评师用ALL_SOURCE查函数体,看到V_KEY_RAW := HEXTORAW('0123...'),直接开不符合项。

原因:密钥写在代码里,任何有DBA_SOURCE权限的人都能看到,等于没加密。

解决:密钥必须放在独立的表里,表只给函数属主读权限,其他用户无任何权限。函数用AUTHID DEFINER(默认)以属主身份执行,调用者看不到表。更严格的做法是密钥放在数据库外,通过 Oracle Wallet 或应用传入,但那样函数就不能独立运行了。折中方案是密钥表 + 定期轮换 + 审计密钥表的访问。

4.4 避坑四:中文乱码,解密出来是问号

现象:加密'张三',解密出来是'??'或乱码。

原因:UTL_I18N.STRING_TO_RAW的字符集参数和数据库字符集不一致。如果数据库是 ZHS16GBK,用AL32UTF8转再转回来可能丢字符。

解决:统一用AL32UTF8,并且确保数据库字符集是AL32UTF8。如果数据库是 GBK,用ZHS16GBK参数,但跨库迁移时会出问题。最稳的是在函数里显式指定AL32UTF8,并且建库时就选 UTF8。

4.5 避坑五:函数在 SQL 里调用,并行查询时结果错乱

现象:开并行查询后,同一行数据解密结果偶尔不对。

原因:如果函数里用了包变量或全局临时表存中间状态,并行会话之间会互相干扰。

解决:函数必须是纯函数,不依赖任何会话级可变状态。IV 每次随机生成,不存包变量。密钥从表读,不缓存到包变量。如果一定要缓存,用DBMS_SESSION的上下文,但并行下也不可靠。最稳的就是每次读表,性能损耗用结果缓存(RESULT_CACHE)补。

5. 进阶:用 RESULT_CACHE 和哈希列把性能拉回可用区间

函数调用慢,核心原因是每行都要执行 PL/SQL 逻辑。如果同一个明文反复加密(比如批量导入时),可以用RESULT_CACHE缓存结果。但加密函数有随机 IV,每次结果不同,不能直接缓存。能缓存的是解密函数:同一个密文解密结果固定。

CREATE OR REPLACE FUNCTION SEC_CRYPTO.F_DECRYPT( P_CIPHER IN VARCHAR2, P_KEY_ID IN VARCHAR2 ) RETURN VARCHAR2 RESULT_CACHE RELIES_ON (SEC_CRYPTO.T_KEY_STORE) IS -- 函数体同上 BEGIN -- ... END; /

RESULT_CACHE会把输入输出对缓存在 SGA 里,下次相同密文直接返回。RELIES_ON告诉 Oracle 如果T_KEY_STORE变了,缓存失效。注意:RESULT_CACHE对每次 IV 不同的加密函数无效,只对解密有效。而且缓存占 SGA 内存,大表全量解密时可能把共享池挤爆,要配合RESULT_CACHE_MAX_SIZE调。

另一个技巧是哈希列 + 函数索引。如果业务必须按加密列查,可以建函数索引:

CREATE INDEX IDX_ORDER_NAME_HASH ON APP_USER.T_ORDER (STANDARD_HASH(CUSTOMER_NAME, 'SHA256'));

但这样查的时候必须用完全一样的表达式:

SELECT * FROM APP_USER.T_ORDER WHERE STANDARD_HASH(CUSTOMER_NAME, 'SHA256') = STANDARD_HASH('张三', 'SHA256');

实测在百万行表上,走函数索引的等值查询能到 50ms 以内,比全表扫描快两个数量级。代价是索引本身占空间,而且STANDARD_HASH对大小写敏感,业务侧要统一大小写。

最后说一个我自己的习惯:每次上线加密函数前,先用DBMS_PROFILER跑一遍典型查询,看函数调用占总时间的比例。如果超过 30%,就别硬扛,把脱敏和查询逻辑挪到应用层。数据库擅长存储和事务,不擅长逐行计算。加密存储该做,但别让数据库一个人扛所有事。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询