简介这份资源面向 Oracle 数据库开发与运维人员提供一套自定义加密解密函数用于解决敏感数据脱敏、加密存储与合规传输问题。包内共 3 个文件以 2 个 sql 脚本和 1 个 txt 说明文档为主压缩包约 5KBsql 脚本分别对应加密与解密逻辑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返回 RAWPL/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%就别硬扛把脱敏和查询逻辑挪到应用层。数据库擅长存储和事务不擅长逐行计算。加密存储该做但别让数据库一个人扛所有事。希望帮到你。本文还有配套的精品资源点击获取