简介:面向 Oracle 数据库管理员与开发人员的一套自定义加密解密函数方案,基于 DES 加密标准对敏感字段进行加密存储与智能脱敏处理,既保护数据隐私,又不影响后续数据分析与挖掘的准确性,可覆盖账户信息、身份证、密码、交易记录等典型数据,适用于金融、医疗、电商等高合规要求场景。压缩包共 3 个文件,包括 2 个 SQL 脚本与 1 个说明文档,整体仅 5KB,结构精简;脚本内置 ENCRYPT_DES 加密与 DECRYPT_DES 解密函数,支持灵活配置密钥与数据长度,附带完整注释与 Readme 使用说明,可快速集成到本地或云数据库环境,显著降低集成与维护成本。函数库经严格测试与优化,稳定可靠,有效满足字段级加密存储、数据脱敏及安全合规的落地需求。目前已有 521 人学习/下载,适合需要低成本增强数据隐私保护的团队直接参考或二次开发。
1. 等保测评和审计要求下,为什么最终自己写Oracle加解密函数
等保测评抽到用户表,前一百行手机号身份证全是明文,当天直接记了高风险。从那一刻起,Oracle自定义加密解密函数就是我数据库安全改造的首选方案,它既能满足“加密存储”这个合规检查项,又能把脱敏规则做成库内视图,敏感数据不会在应用层裸奔。过去两年我帮业务系统做过三套类似的加解密改造,结论都是同一个:TDE透明加密解决不了“查询时要脱敏、导出时要解密”这类细粒度需求,最终还是得在Oracle里写一套自己的函数来收口。
这套方案适合三类人:被等保整改和内部审计追着改库的DBA,报表和运维账号需要访问生产数据但又不能看明文的安全负责人,以及接手的系统里敏感字段已经明文裸奔多年的应用开发。后面所有内容都围绕一条主线展开:库内如何加密、查询如何解密、脱敏怎么展示、脏数据怎么兜底,以及上线前哪些坑必须排掉。
2. 自定义加解密函数的内核:DBMS_CRYPTO封装、密钥与IV参数设计
2.1 为什么把TDE放一边,选择DBMS_CRYPTO做内核
Oracle自带的TDE(Transparent Data Encryption)解决的是数据文件被拖走之后的存储加密问题,磁盘上的文件是密文,但应用连上数据库一查,返回的还是明文。等保测评里“敏感数据加密存储”这一项TDE能过,可“运维账号只允许看到脱敏数据”“生产数据导出到测试库前必须打码”这些诉求,TDE完全接不住。
DBMS_CRYPTO是Oracle内置的PL/SQL包,提供AES、3DES等对称加密算法和SHA系列哈希算法。它不是某个独立安装的组件,而是数据库自带的包,所以生产环境只要授权就能用。常见做法是在这个包基础上包一层自定义函数,把密钥读取、IV生成、十六进制转码全部封装在函数体内,业务SQL只需要调用一个fun_enc_aes()或fun_dec_aes(),不需要关心底层算法和密钥参数。
真正让我放弃直接在代码里调DBMS_CRYPTO.ENCRYPT的核心原因是:每次加密都要写一遍密钥读取和参数拼装,十几个Procurement模块里各写各的,算法不统一,密钥散落得到处都是。集中成一套自定义函数之后,所有业务统一走同一套入口,后面换算法、轮换密钥都只改一个包。
2.2 加密函数与解密函数的最小实现
这是我在生产环境里用过的最小可用版本。密文格式统一为ENC:前缀加IV加密文,全部转十六进制字符串存储。
CREATE OR REPLACE FUNCTION fun_enc_aes(p_plaintext IN VARCHAR2) RETURN VARCHAR2 IS v_key_raw RAW(32); v_iv_raw RAW(16); v_cipher RAW(4000); v_result VARCHAR2(4000); BEGIN -- 密钥从密钥表读取,按密钥版本取当前生效的256位密钥 SELECT key_raw INTO v_key_raw FROM sec_key_t WHERE key_version = '20250101'; -- 每次加密生成随机IV,避免相同明文产生相同密文 v_iv_raw := DBMS_CRYPTO.RANDOMBYTES(16); v_cipher := DBMS_CRYPTO.ENCRYPT( src => UTL_RAW.CAST_TO_RAW(p_plaintext), typ => DBMS_CRYPTO.ENCRYPT_AES256 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5, key => v_key_raw, iv => v_iv_raw ); -- 返回格式:ENC: + IV十六进制串 + 密文十六进制串 v_result := 'ENC:' || RAWTOHEX(v_iv_raw) || RAWTOHEX(v_cipher); RETURN v_result; END fun_enc_aes; /解密函数是对称的逆操作,输入带ENC:前缀的密文,输出明文;遇到无法解密的数据直接返回原串,不报错中断。
CREATE OR REPLACE FUNCTION fun_dec_aes(p_cipher_text IN VARCHAR2) RETURN VARCHAR2 IS v_key_raw RAW(32); v_iv_raw RAW(16); v_cipher RAW(4000); v_plain VARCHAR2(4000); v_body VARCHAR2(4000); BEGIN -- 非本函数生成的字符串直接原样返回,由调用方判断是否脏数据 IF p_cipher_text IS NULL OR SUBSTR(p_cipher_text, 1, 4) <> 'ENC:' THEN RETURN p_cipher_text; END IF; SELECT key_raw INTO v_key_raw FROM sec_key_t WHERE key_version = '20250101'; -- 去掉ENC:前缀后,前32个十六进制字符是16字节IV v_body := SUBSTR(p_cipher_text, 5); v_iv_raw := HEXTORAW(SUBSTR(v_body, 1, 32)); v_cipher := HEXTORAW(SUBSTR(v_body, 33)); v_plain := UTL_RAW.CAST_TO_VARCHAR2( DBMS_CRYPTO.DECRYPT( src => v_cipher, typ => DBMS_CRYPTO.ENCRYPT_AES256 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5, key => v_key_raw, iv => v_iv_raw ) ); RETURN v_plain; EXCEPTION WHEN OTHERS THEN -- 脏数据兜底:返回原串,避免业务SQL因为一条坏数据整体中断 RETURN p_cipher_text; END fun_dec_aes; /这里有两个参数需要在首次上线前理解清楚。第一,ENCRYPT_AES256 + CHAIN_CBC + PAD_PKCS5是三个常量相加,表示使用AES 256位密钥、CBC分组链模式、PKCS5填充。Oracle的DBMS_CRYPTO里,算法、链模式、填充方式通过这种加法组合传入,缺一个都编译不过。第二,每次加密都调用RANDOMBYTES(16)生成新的随机IV,再把IV拼在密文前面一起存,保证了同一个手机号两次加密出来的密文完全不一样。如果把IV固定写死,攻击者就能通过比对密文判断“这两行是不是同一个手机号”,脱敏就失去了意义。
2.3 密钥不写进代码:密钥表与Package封装
函数体里那句SELECT key_raw FROM sec_key_t是整个方案的关键设计。密钥不能写死在函数代码里,否则一个开发人员查看函数定义就等于拿到了密钥。生产环境的常见做法是建一张密钥表,每次轮换密钥就往里插一条新版本记录,函数只读取当前生效的版本。
CREATE TABLE sec_key_t ( key_version VARCHAR2(8) PRIMARY KEY, key_raw RAW(32) NOT NULL, active_flag CHAR(1) DEFAULT 'Y' NOT NULL );密钥生成可以用Oracle的DBMS_RANDOM,也可以用操作系统熵源生成32字节随机十六进制串后插入。注意不要自己在键盘上敲一个“看起来像密钥”的字符串,那东西往往有规律。
外层再包一个Package,把加密、解密、哈希三个操作做成包内函数。业务方只授权Package的执行权限,不开放对sec_key_t表的直接查询。这个Package结构同时对应了Oracle存储过程和函数大全里常见的工程模式:包头部只暴露三个函数名,包体里具体实现加解密逻辑和密钥读取。后面换算法、改密钥版本,业务SQL不需要改动,这是自定义函数比TDE更适合做统一收口的核心原因。
3. 从密文存储到脱敏展示:视图、触发器和业务查询的三种接法
3.1 字段设计思路与脱敏视图
改表之前先设计好敏感字段的存储格式。我在生产上用的规则是:手机号、身份证号、银行卡号这类长度有限且格式固定的字段,一律存成VARCHAR2,插入时调fun_enc_aes()转密文;像备注、地址这种长文本,暂时不纳入加密范围,等密钥轮换机制跑顺了再逐步覆盖。
加密后的字段没法直接看,所以必须给查询入口做两种视图。一种是给前台业务用的脱敏视图,解密后只展示部分字符;另一种是给审计和风控用的全量解密视图,权限严格控制。
脱敏视图的写法看起来简单,但有几个细节必须注意。
CREATE OR REPLACE VIEW v_user_masked AS SELECT u.user_id, u.user_name, SUBSTR(t.mobile, 1, 3) || '****' || SUBSTR(t.mobile, 8) AS mobile_masked, CASE WHEN LENGTH(t.id_card) >= 18 THEN SUBSTR(t.id_card, 1, 6) || '********' || SUBSTR(t.id_card, 15) ELSE 'UNMASKED_ABNORMAL' END AS id_card_masked FROM t_user u JOIN ( SELECT user_id, CASE WHEN mobile_enc LIKE 'ENC:%' THEN fun_dec_aes(mobile_enc) ELSE mobile_enc END AS mobile, CASE WHEN id_card_enc LIKE 'ENC:%' THEN fun_dec_aes(id_card_enc) ELSE id_card_enc END AS id_card FROM t_user ) t ON t.user_id = u.user_id;这里有一个踩过多次的坑:不要在视图里直接写fun_dec_aes(mobile_enc)三次,否则Oracle会对同一行数据执行三次解密。视图里的子查询先把每一行解密一次,外层再计算掩码,这样每行只做一次解密。mobile_enc LIKE 'ENC:%'这个判断也重要,它保证历史明文数据在没来得及加密的过渡期也能正常显示,不会因为解密函数返回原串而双倍截断。
掩码规则按业务要求来,我常用的是手机号保留前三位和后四位,中间四位用星号;身份证号保留前六位和后四位。LENGTH判断是为了筛掉脏数据,如果解密出来长度不对,宁可显示UNMASKED_ABNORMAL也不要让脏数据流到前端。
3.2 触发器接入:让存量应用无感写入密文
存量系统最麻烦的是应用代码里几十处INSERT和UPDATE都是明文写入,如果要求开发逐一改代码,排期至少两个月。短平快的做法是加一个BEFORE INSERT触发器,在数据落库前自动把敏感字段转成密文。
CREATE OR REPLACE TRIGGER trg_user_encrypt BEFORE INSERT OR UPDATE ON t_user FOR EACH ROW BEGIN IF :NEW.mobile_enc IS NOT NULL AND :NEW.mobile_enc NOT LIKE 'ENC:%' THEN :NEW.mobile_enc := fun_enc_aes(:NEW.mobile_enc); END IF; IF :NEW.id_card_enc IS NOT NULL AND :NEW.id_card_enc NOT LIKE 'ENC:%' THEN :NEW.id_card_enc := fun_enc_aes(:NEW.id_card_enc); END IF; END; /触发器里的NOT LIKE 'ENC:%'判断是防止二次加密的关键。如果应用层已经开始调fun_enc_aes(),传进来的已经是密文,触发器再做一次加密就会变成“密文套密文”,解密后拿到一段乱码而不是明文。这种问题在生产环境出现过不止一次,排查起来非常像玄学:明明加密函数测试没问题,写进库里就是解不出来。
触发器方案适合过渡期,不适合长期依赖。因为它会把加密逻辑藏在DML语句背后,开发看代码时不知道字段已经加密,容易在报表SQL里直接对密文做字符串截断,出来的结果完全不可读。我的建议是触发器只用来做存量切换,等应用代码全部改造完毕后择机停用。
3.3 Java与Python侧读密文的调用方式
接口项目读取密文数据有两条路。一条路是Java在JDBC里执行SQL时调用解密函数,一条路是Python用oracledb直接查视图。生产上我更推荐Java和Python项目不要自己维护密钥,而是把解密留在Oracle视图或函数里。
Java侧典型写法:
SELECT user_id, fun_dec_aes(mobile_enc) AS mobile FROM t_user WHERE user_id = :id;把fun_dec_aes()直接写进SQL里,Java端拿到的就是明文。好处是密钥永远不离开数据库,应用服务器被拖库也拿不到密钥。
Python侧我用oracledb连接Oracle之后,同样把解密函数放在SQL里:
import oracledb conn = oracledb.connect(user="app_user", password="***", dsn="dbhost:1521/orcl") cur = conn.cursor() cur.execute( "SELECT user_id, fun_dec_aes(mobile_enc) AS mobile FROM t_user WHERE user_id = :uid", {"uid": 1001}, ) for row in cur: print(row[0], row[1])如果API返回给前端的就是脱敏数据,直接查v_user_masked视图;如果内部系统需要拿完整明文做二次加工,再走函数解密。把视图和函数分开授权,是这套方案里控制数据暴露面的核心手段。
4. 中文、超长、脏数据兼容:把加密函数打磨到能直接上生产
4.1 字符集与长度翻倍:先算清楚再定列类型
加解密改造翻车最多的场景不是算法选错,而是列长度不够。VARCHAR2按字节存储,AL32UTF8字符集下一个汉字占3字节;AES按16字节分组加密,加密后还要转十六进制字符串,长度直接翻倍。一个18位身份证号加密后变成96个字符,一个11位手机号加密后变成68个字符。
长度估算可以用这条SQL实测:
SELECT LENGTH(fun_enc_aes('13800138000')) FROM dual; -- 手机号 SELECT LENGTH(fun_enc_aes('110101199003070011')) FROM dual; -- 身份证号 SELECT LENGTH(fun_enc_aes('张三丰')) FROM dual; -- 中文姓名测试结果会让你立刻意识到,原来VARCHAR2(18)的身份证字段改完后至少要VARCHAR2(128)才保险。生产环境我一般在估算值基础上再加三成余量,同时把列默认值改成NULL,避免历史数据迁移时因为长度报ORA-12899把整个批次回滚。
另一个常见的坑是UTL_RAW.CAST_TO_RAW对空字符串的处理。fun_enc_aes(NULL)会返回NULL,但fun_enc_aes('')在某些Oracle版本里会报错。函数里最好在入口加一层判断:
IF p_plaintext IS NULL OR p_plaintext = '' THEN RETURN NULL; END IF;空值加密没意义,统一返回NULL,后续解密也会因为前缀判断返回NULL,整个链路不会断。
4.2 解密失败兜底:ORA-28817与脏数据并存
生产数据永远比测试数据脏得多。最常见的坏数据有两种:一是历史遗留的明文混进了密文列,二是应用层误把截断后的密文写入了字段。后者尤其隐蔽,比如一个密文本来有100个字符,代码里用了SUBSTR截断后只剩60个字符,解密时块长度不对,直接报ORA-28817。
ORA-28817是Oracle解密时最常见的报错之一,意思是“输入数据不是块大小的整数倍”。AES按16字节分组,密文长度必须是16的倍数,截断后自然过不了这一关。
解决方案分两层。第一层是第2章给出的EXCEPTION WHEN OTHERS THEN RETURN p_cipher_text,保证单条坏数据不会让整个查询中断;第二层是在视图里对解密结果做二次校验,比如手机号正则检查、身份证长度检查。坏数据返回原串后,视图层发现它不是合法格式,返回INVALID_ENCRYPTED_DATA,这样报表组不会拿脏数据去填充业务字段。
还有一类输入会让HEXTORAW直接报错,比如密文里混进了非十六进制字符。这种情况同样被WHEN OTHERS捕获,返回原串。不要把这条兜底逻辑去掉,否则一条坏数据就能让全表查询报ORA-28817,这种生产事故我经历过一次,教训很深。
4.3 密文不能检索:影子列与等值匹配方案
加密存储有一个绕不开的代价:WHERE mobile_enc = '13800138000'永远查不到数据。密文无法倒推明文,Oracle也没法对密文做等值匹配。业务系统最常见的诉求是“按手机号精确查用户”,如果每次都要解密全表再比对,数据量大一点就卡死。
我一般会加一个影子列存明文哈希值,精确查询走哈希匹配。哈希不可逆,但相同输入永远得到相同输出,等值匹配完全够用。
ALTER TABLE t_user ADD (mobile_digest RAW(32)); UPDATE t_user SET mobile_digest = DBMS_CRYPTO.HASH( UTL_RAW.CAST_TO_RAW('13800138000'), DBMS_CRYPTO.HASH_SH256 ) WHERE user_id = 1001;查询时先对输入值做同样的哈希运算,再匹配影子列:
SELECT user_id, fun_dec_aes(mobile_enc) AS mobile FROM t_user WHERE mobile_digest = DBMS_CRYPTO.HASH( UTL_RAW.CAST_TO_RAW('13800138000'), DBMS_CRYPTO.HASH_SH256 );影子列要建普通索引,查询可以用索引快速定位到目标行,再解密那一条的真实数据。这里不需要担心哈希碰撞,SHA-256的碰撞概率在实际业务体量下可以忽略。需要注意影子列不能代替密文列,它只是为了解决等值检索效率问题,哈希值本身不能还原明文,所以即使泄露也不会造成数据泄露。
模糊查询场景影子列也无解。手机号后四位查询、地址关键字搜索这类需求,在加密方案下只能回到脱敏视图里做全表解密,或者把模糊查询限定到非敏感字段上。这是加密存储的物理边界,架构评审时一定要提前告诉业务方,否则上线后需求方拿着模糊搜索的工单来找你,非常被动。
5. 上线前必须排查的5个坑:权限、类型、索引与批量性能清单
5.1 DBMS_CRYPTO权限不足,业务账号一调用就ORA-01031
现象:DBA账号调用加密函数正常,业务账号一调用就报ORA-01031,提示权限不足。
原因:DBMS_CRYPTO这个包在Oracle里默认只授权给了DBA和SYS用户,普通业务账号虽然有自定义函数的执行权限,但函数内部引用DBMS_CRYPTO时,以函数定义者身份执行,仍然需要访问SYS.DBMS_CRYPTO的权限。
解决:给需要的业务账号单独授权,不要在PUBLIC上一刀切:
GRANT EXECUTE ON SYS.DBMS_CRYPTO TO app_user; GRANT SELECT ON sec_key_t TO app_user;同时确认函数创建时用的是定义者权限。如果函数用了AUTHID CURRENT_USER,调用者的权限不足问题会更复杂,生产环境默认不要用调用者权限。这个坑用一句话总结:函数能编译通过不代表业务账号能执行,一定要用最小权限账号做一次真实调用测试。
5.2 VARCHAR2长度不足,中文密文入库报ORA-12899
现象:应用插入一条中文地址,应用日志完全没有异常,数据库端报ORA-12899,提示列值超出最大长度。
原因:加密函数返回的十六进制密文长度是明文长度按AES分组扩展后再翻倍的结果。原来存地址的VARCHAR2(200)列,加密后长度可能达到500以上,直接插入必然报错。
解决:先做线上数据长度分析,取敏感字段最长值的加密结果,再按结果调整列定义:
SELECT MAX(LENGTH(mobile_enc)), MAX(LENGTH(id_card_enc)) FROM t_user;如果列上有依赖长度的索引,建议把列类型调整到VARCHAR2(512)并同步重建索引。这个改动通常要申请变更窗口,不要在业务高峰期直接执行。历史数据迁移时更要先跑一轮大数据量演练,确认所有列的容量都够,再动生产。
5.3 对密文列建索引也没用,WHERE条件查不到数据
现象:开发给密文列建了索引,查询还是慢,WHERE条件里写明文也查不到数据。
原因:索引里保存的是完整密文字符串,而应用SQL里传的是'13800138000',两者完全不匹配,索引直接失效。这是加密存储的天然限制,不是Oracle的bug。
解决:精确查询用第4章的影子列哈希方案。WHERE mobile_digest = DBMS_CRYPTO.HASH(...)能走索引,此时再通过user_id回表取密文解密。如果业务场景要求“先把明文加密成密文再查询”,也可以这样写:
SELECT user_id, fun_dec_aes(mobile_enc) AS mobile FROM t_user WHERE mobile_enc = fun_enc_aes('13800138000');这种方式在数据量小的表上没问题,但每次执行都调用一次加密函数,数据量大了性能扛不住。生产环境优先用影子列,别在核心查询里对函数结果做等值匹配。
5.4 百万行存量明文批量加密,跑了几个小时还锁表
现象:用一条大UPDATE把存量数据从明文改成密文,执行了三个小时没结束,会话出现大量锁等待,应用写入全部阻塞。
原因:单条大UPDATE会产生巨大的undo和redo,且整个过程持有一堆行锁。函数内部每次加密会查密钥表和生成随机IV,开销比普通字符串更新高一个数量级,大事务问题被进一步放大。
解决:分页批量提交,每处理一批就COMMIT一次。我用的是游标批量取数加FORALL的方式:
DECLARE CURSOR c_old IS SELECT user_id, mobile_enc FROM t_user WHERE mobile_enc NOT LIKE 'ENC:%'; TYPE t_id_tab IS TABLE OF t_user.user_id%TYPE; TYPE t_val_tab IS TABLE OF t_user.mobile_enc%TYPE; v_ids t_id_tab; v_vals t_val_tab; v_batch CONSTANT PLS_INTEGER := 5000; BEGIN OPEN c_old; LOOP FETCH c_old BULK COLLECT INTO v_ids, v_vals LIMIT v_batch; EXIT WHEN v_ids.COUNT = 0; FORALL i IN 1..v_ids.COUNT UPDATE t_user SET mobile_enc = fun_enc_aes(v_vals(i)) WHERE user_id = v_ids(i); COMMIT; END LOOP; CLOSE c_old; END; /FORALL减少SQL上下文切换,每5000行提交一次控制undo占用。如果业务允许,还可以关闭对该表的外键和触发器后再批量处理,处理完恢复。迁移期间需要评估应用有没有写入,否则会出现新数据被旧批次覆盖的问题。
5.5 视图里函数重复解密,查询慢到无法上线
现象:脱敏视图上线后,一个原本几百毫秒的查询变成几秒,数据库CPU飙升。
原因:视图里多个字段各自调用解密函数,Oracle没有做结果复用。SUBSTR(fun_dec_aes(mobile_enc),1,3)和SUBSTR(fun_dec_aes(mobile_enc),8)会触发两次完整解密,如果还有身份证、银行卡字段,一行数据被解密五六次。
解决:在视图里先做一层子查询,把解密结果提前算出,外层再格式化:
CREATE OR REPLACE VIEW v_user_masked AS SELECT t.user_id, SUBSTR(t.mobile, 1, 3) || '****' || SUBSTR(t.mobile, 8) AS mobile_masked FROM ( SELECT user_id, CASE WHEN mobile_enc LIKE 'ENC:%' THEN fun_dec_aes(mobile_enc) ELSE mobile_enc END AS mobile FROM t_user ) t;这样一个用户只解密一次。视图层的函数调用排查起来费时间,因为执行计划里看不到函数内部的开销,只看到CPU消耗异常。建议在测试环境用DBMS_PROFILER或者直接在函数里加计时日志,把热点定位到具体SQL。这种问题一旦上线再优化,往往要等下一个变更窗口,前期设计阶段就按“最多解密一次”的标准写视图,后期会省很多事。
6. 回归验证与密钥轮换:顺手把加解密函数做成例行任务
6.1 用一张验证脚本,把加解密往返测出置信度
每次改动函数或者迁移数据之后,都要跑一轮回归验证。我习惯把样本数据做成一张临时表,覆盖空值、中文、超长字符、特殊符号、历史明文这五类数据,然后逐一验证加密后能解密回原文。最小验证脚本可以这样写:
SELECT fun_dec_aes(fun_enc_aes('test_mobile_138')) FROM dual; SELECT fun_dec_aes(fun_enc_aes('张三丰')) FROM dual; SELECT fun_dec_aes(fun_enc_aes(NULL)) FROM dual; SELECT fun_dec_aes(fun_enc_aes('abc!@#$%^&*()_+')) FROM dual;跑完看输出是否与输入一致。NULL那条应该返回NULL,超长字符那条要确认不报ORA-06502。这条验证在每次密钥轮换后都必须重跑一遍,确保新密钥函数与旧密文的兼容性。
6.2 按月轮换密钥,历史密文不受影响
密钥不能永远不换。常见做法是按月生成新密钥版本,密钥表里插入新记录,函数改为读取最新版本:
INSERT INTO sec_key_t(key_version, key_raw, active_flag) VALUES ('20250201', SYS.DBMS_RANDOM.RANDOMBYTES(32), 'Y'); UPDATE sec_key_t SET active_flag = 'N' WHERE key_version <> '20250201';这里有一个必须提前规划的问题:旧密文是用旧密钥加密的,新函数读新密钥后,老数据全部解不出来。生产上稳妥的做法是解密函数加一个根据密钥版本重试的逻辑,第一次用新密钥解,失败后查密钥表里还有没有旧版本密钥可用。如果业务量不大,直接选择在凌晨低峰期对全表重新加密一次,把老密文全部解密再加密成新密钥,一劳永逸。轮换窗口要选在业务空闲时段,并提前验证回滚方案,一旦新密钥异常,立即用旧密钥版本恢复。
6.3 DBA的例行回归习惯
密钥轮换不是把新密钥插进表里就完事了。我给自己定的例行流程是:轮换前备份密钥表,轮换后随机抽三到五条生产密文做解密比对,第二天早上再看告警日志有没有ORA-28817出现。同时对影子列做一次抽样校验,确认mobile_digest和mobile_enc对应关系没有因为应用双写而错位。
把TRUNC(SYSDATE)作为密钥版本号的一部分,可以让密钥历史一眼看清。比如20250201就代表2025年2月1日生效的密钥,回滚时顺着版本号往前找就行,不用翻变更记录。这套方案跑熟之后,每次等保检查只需要把密钥表和脱敏视图的权限清单交出去,加解密部分基本不用再做额外解释。
我做这类加解密改造最深的教训,是永远不要只测“加密-解密”这一条正路。脏数据、空值、长度、权限这些边角才是生产环境真正咬人的地方,回归脚本里每一类都要带上。希望帮到你。
本文还有配套的精品资源,点击获取