# 信创改造不给工期?注解驱动SM4加解密,前端一行代码不用改
信创改造不给工期注解驱动SM4加解密前端一行代码不用改非科班野生程序员深耕政务信息化20年这套自研Java Web框架支撑过省级新农保、全国首例跨省医保结算等核心民生系统18年稳定运行至今。这篇复盘信创国密改造的设计思路全是政务场景踩坑后的实用解法不求优雅但求落地。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景信创改造来了。要求敏感字段身份证号、密码、姓名、账号等入库必须加密存储读取时解密还原数据必须加签验签防篡改。听起来不复杂但问题是我的系统已经跑了十几年几十个业务模块几百张表前端页面更是多得数不清。如果每个业务模块、每个接口、每个前端页面都改一遍那工期不可控而且很容易漏改、改错。我的目标是业务代码包括前端零改动只加注解就能实现加解密和加解签。问题拆解信创国密改造要解决三个问题入库前加密敏感字段写入数据库前用 SM4 加密出库后解密从数据库读出来后用 SM4 解密业务代码拿到的还是明文防篡改验签关键字段加签MAC读出时验签数据被改过就能发现关键约束前端不能改——前端传的还是明文收到的也必须是明文业务代码不能改——Service 层的代码不感知加解密的存在可开关——没上信创的环境关掉配置就回到明文模式粒度控制——不是所有表、所有字段都要加密只加在需要的地方设计思路核心思路用注解标记哪些 Dao 类的哪些字段需要加密/加签在 DBUtil 的读写管道里自动处理。整个方案由三个层面组成┌─────────────────────────────────────────┐ │ 业务代码 / 前端 │ │ 完全不感知加解密的存在 │ └──────────────────┬──────────────────────┘ │ 明文数据 ┌──────────────────▼──────────────────────┐ │ DBUtilORM 层 │ │ │ │ 写入管道 │ │ SaveDao() → Mac() enCode() → 入库 │ │ 先加签再加密密文入库 │ │ │ │ 读取管道 │ │ getDao() → deCode() verMac() → 返回 │ │ 先解密再验签明文返回 │ └──────────────────┬──────────────────────┘ │ 密文数据 ┌──────────────────▼──────────────────────┐ │ SM4 国密加密服务 │ │ 独立部署的密码机 / 密码网关 │ └─────────────────────────────────────────┘第一层两个注解myCode——标记需要加解密的字段Target({ElementType.TYPE,ElementType.FIELD})Retention(RetentionPolicy.RUNTIME)publicinterfacemyCode{}myMac——标记需要加签验签的字段Target({ElementType.TYPE,ElementType.FIELD})Retention(RetentionPolicy.RUNTIME)publicinterfacemyMac{}注解打在类上表示这个 Dao 参与加解密流程打在字段上表示这个字段需要加密/加签。以用户表为例publicclassUserextendsDao{myCodemyMacprivateStringpsn_account;myCodemyMacprivateStringpsn_pwd;myCodemyMacprivateStringpsn_name;myCodemyMacprivateStringpsn_idcard;// 身份证号// 其他字段不加注解不参与加解密privateStringpsn_sex;privateStringpsn_email;privateStringpsn_phone;// ...}加了myCode的字段会自动加解密加了myMac的字段会自动加签验签。没加注解的字段完全不受影响。第二层DBUtil 管道——写入时自动加密加签SaveDao()——写入管道当业务代码调用DBUtil.SaveDao()保存数据时在执行 SQL 之前自动完成加签和加密publicstaticvoidSaveDao(Classcalss,StringMethodNmae,Objectparams)throwsutilException{// ...if(paramsinstanceofDao){DaodaoObj(Dao)params;try{// 先加签Mac(daoObj);// 再加密enCode(daoObj);}catch(Exceptione){e.printStackTrace();}}// 加密后的 Dao 对象传入 MyBatis 执行 SQL// 数据库里存的是密文// ...}执行顺序是先加签再加密。因为加签是对明文进行的确保签名的是原始数据加密是对明文签名一起进行的。同样getDao()入参时也有加密处理if(params[0]instanceofDao){// 入参加密try{enCode((Dao)params[0]);}catch(Exceptione){e.printStackTrace();}}这是因为getDao()传入的查询参数也可能是敏感字段比如用身份证号查用户需要加密后再拼 SQL 去查否则查不到数据库里存的是密文。第三层DBUtil 管道——读取时自动解密验签getDao()——读取管道当业务代码调用DBUtil.getDao()查询数据时在返回结果之后自动完成解密和验签if(result!nullresult.size()0result.get(0)instanceofDao){try{// 先解密deCode(result);}catch(Exceptione){e.printStackTrace();}// 再验签try{verMac(result);}catch(Exceptione){thrownewutilException(e.getMessage(),-8998);}}returnresult;执行顺序是先解密再验签。因为数据库里存的是密文密文签名先解密还原出明文再拿明文重新算签名和存储的签名比对。getOne()返回单个对象时也做了同样处理if(resultinstanceofDao){ListDaolistnewArrayListDao();list.add((Dao)result);try{deCode(list);}catch(Exceptione){e.printStackTrace();}try{verMac(list);}catch(Exceptione){thrownewutilException(e.getMessage(),-8998);}}第四层enCode/deCode/Mac/verMac 四个方法enCode()——单个 Dao 对象加密publicstaticvoidenCode(DaoobjDao)throwsException{// 总开关配置文件里 usageCode1 才启用if(!1.equals(configUtil.get(usageCode))){return;}// 检查类上有没有 myCode 注解if(objDao.getClass().isAnnotationPresent(myCode.class)){Field[]fieldsobjDao.getClass().getDeclaredFields();StringBufferbuffer1newStringBuffer();// 遍历所有字段找出带 myCode 的for(Fieldfield:fields){if(field.isAnnotationPresent(myCode.class)){field.setAccessible(true);StringvalueString.valueOf(field.get(objDao));field.setAccessible(false);if(value!null!.equals(value)!null.equals(value)){// 先 Base64 编码Stringbase64ValueBase64.encode(value.getBytes());buffer1.append(\field.getName()\:\base64Value\,);}}}Stringdatabuffer1.toString();datadata.substring(0,data.length()-1);// 调用 SM4 加密服务JSONObjectjsonObjectSM4.enCode(data);// 把加密后的值写回 Dao 对象for(Fieldfield:fields){if(field.isAnnotationPresent(myCode.class)){field.setAccessible(true);StringvaluejsonObject.getString(field.getName());field.set(objDao,value);field.setAccessible(false);}}}}关键点加密是字段级别的但 SM4 调用是一次性的。它把所有需要加密的字段拼成一个 JSON一次调用密码机再把返回的密文一一写回对应字段。不是每个字段调一次密码机。deCode()——批量解密publicstaticvoiddeCode(ListDaoresult)throwsException{if(!1.equals(configUtil.get(usageCode))){return;}for(DaoobjDao:result){deOne(objDao);}}deOne()和enCode()是反过程把密文字段拼成 JSON → 调用SM4.deCode()→ 把解密后的值 Base64 解码 → 写回 Dao 对象。Mac()——加签publicstaticvoidMac(DaoobjDao)throwsException{if(!1.equals(configUtil.get(usageCode))){return;}if(objDao.getClass().isAnnotationPresent(myMac.class)){Field[]fieldsobjDao.getClass().getDeclaredFields();StringBufferbuffer1newStringBuffer();for(Fieldfield:fields){if(field.isAnnotationPresent(myMac.class)){field.setAccessible(true);StringvalueString.valueOf(field.get(objDao));field.setAccessible(false);if(value!null!.equals(value)!null.equals(value)){Stringbase64ValueBase64.encode(value.getBytes());buffer1.append(base64Value);}}}Stringdatabuffer1.toString();JSONObjectjsonObjectSM4.Mac(data);// 把签名值和 IV 向量存到 Dao 的公共字段里objDao.setMac(jsonObject.getString(mac));objDao.setIv(jsonObject.getString(iv));}}加签和加密不同加签不是按字段单独签的而是把所有myMac字段的值 Base64 拼接成一个字符串算一次 MAC。mac和iv存到 Dao 基类的公共字段里随数据一起入库。Dao 基类里定义了这两个字段publicclassDao{privateStringiv;// 签名 iv 向量privateStringmac;// 签名 mac// ...}verMac()——验签publicstaticbooleanverMac(ListDaoresult)throwsException{if(!1.equals(configUtil.get(usageCode))){returntrue;// 未启用时直接返回通过}for(DaoobjDao:result){if(objDao.getClass().isAnnotationPresent(myMac.class)){Field[]fieldsobjDao.getClass().getDeclaredFields();StringBufferbuffer1newStringBuffer();for(Fieldfield:fields){if(field.isAnnotationPresent(myMac.class)){field.setAccessible(true);StringvalueString.valueOf(field.get(objDao));field.setAccessible(false);if(value!null!.equals(value)!null.equals(value)){Stringbase64ValueBase64.encode(value.getBytes());buffer1.append(base64Value);}}}// 把明文数据 存储的 mac iv 一起传给密码机验签Stringdata\data\:\buffer1.toString()\,\mac\:\objDao.getMac()\,\iv\:\objDao.getIv()\;booleanbSM4.verMac(data);if(!b){thrownewException(验签失败!);}}}returntrue;}验签失败直接抛异常业务代码会收到错误数据不会返回给前端。第五层SM4 服务——和密码网关通信publicclassSM4{privatefinalstaticStringipAdressconfigUtil.get(GMcode_url);privatefinalstaticStringauthHeaderBasic configUtil.get(GMcode_auth);privatefinalstaticStringtenantCodeconfigUtil.get(GMcode_tenant);privatefinalstaticStringencryptKeyCodeconfigUtil.get(GMcode_encryptKey);privatefinalstaticStringmacKeyCodeconfigUtil.get(GMcode_macKey);publicstaticJSONObjectenCode(StringsubData)throwsException{// 调用密码网关的加密接口// POST /api/v1/cipher/json/encrypt// 传入 keyCode algorithmParam(SM4/ECB/PKCS7Padding) data// 返回加密后的 JSON}publicstaticJSONObjectdeCode(StringsubData)throwsException{// 调用密码网关的解密接口// POST /api/v1/cipher/json/decrypt}publicstaticJSONObjectMac(StringsubData)throwsException{// 调用密码网关的加签接口// POST /api/v1/cipher/mac}publicstaticbooleanverMac(StringsubData)throwsException{// 调用密码网关的验签接口// POST /api/v1/cipher/macVerify}}所有加密密钥、签名密钥、认证信息都从配置文件读取不硬编码。密码运算不在应用内完成而是通过 HTTPS 调用独立部署的密码网关密码机这是信创合规要求的做法。数据流转全过程以一个用户登录场景为例写入用户注册/修改密码前端发送明文 JSON → 框架解析成 User 对象明文 → DBUtil.SaveDao() → Mac()对 psn_account/psn_pwd/psn_name/psn_idcard 计算签名mac/iv 存入 Dao → enCode()对 psn_account/psn_pwd/psn_name/psn_idcard 加密密文写回 Dao → MyBatis 执行 SQL写入数据库的是密文 mac iv读取用户登录/查询数据库查出密文 → MyBatis 映射成 User 对象密文 → DBUtil.getDao() → deCode()对 psn_account/psn_pwd/psn_name/psn_idcard 解密明文写回 Dao → verMac()拿明文重新算签名和存储的 mac/iv 比对 → 返回给业务代码明文 → 前端收到的还是明文 JSON业务代码和前端全程无感。为什么能做到前端零改动关键在于加解密发生在DBUtil 层这个位置选得非常精确往上业务代码和前端看到的永远是明文不需要任何改动往下数据库里存的是密文满足信创合规要求往左Dao 基类的getBlock()方法会屏蔽mac、iv等内部字段不会序列化到前端publicStringgetBlock(){return|block|maxRow|minRow|dialect|level|upInsField|whereField|tableName|DbName|orderByParam|_id|;}mac和iv没有出现在getBlock()里——因为它们被加密后的密文字段值覆盖了前端根本看不到这两个字段的存在。为什么这样做为什么不直接在数据库层面做透明加密数据库透明加密TDE只能解决存储加密的问题不能解决传输加密和防篡改的问题。信创要求的是应用层加解密密码运算必须经过国密认证的密码机。为什么不用 Spring AOP因为我的框架没有用 Spring。但退一步说即使用了 Spring AOP切入点的选择也是一样的——必须在 ORM 层因为这里既知道数据结构Dao 对象的注解又能控制 SQL 执行前后的数据。为什么要全局开关信创改造是分阶段推进的。有的地市先上有的后上。同一个系统部署在不同地方有的要加密有的不要。配置文件里usageCode1开启不配置或配置其他值就跳过所有加密方法开头都做这个判断if(!1.equals(configUtil.get(usageCode))){return;// 直接跳过和没加注解一样}决策原则把加解密做在管道里不要做在业务里。业务代码的职责是处理业务逻辑不应该关心数据是加密存储还是明文存储。加解密是横切关注点应该由框架层统一处理。注解驱动 ORM 管道让业务开发人员只需要做一件事在需要加密的字段上加myCode在需要验签的字段上加myMac。你的项目信创改造是怎么做的是逐个接口改还是也在框架层做了统一处理欢迎评论区聊聊。作者许彰午| 非科班野生程序员深耕政务信息化20年标签#Java #信创 #SM4 #国密 #加解密 #加签验签 #注解驱动 #政务信息化