5分钟掌握Hutool实现SM4国密加密:Base64与Hex编码实战
1. 项目概述为什么选择Hutool处理SM4加密最近在做一个涉及数据安全交换的项目对方要求必须使用国密SM4算法并且密文输出格式要同时支持Base64和Hex十六进制。一开始我琢磨着自己去实现SM4的ECB、CBC模式再处理填充和编码转换但转念一想这种重复造轮子的事太费时间了。正好团队里一直在用Hutool这个国产工具库它号称是“Java开发者的瑞士军刀”我印象中它的加密模块封装得挺全。于是我去翻了一下Hutool的文档发现它果然对国密算法有现成的、开箱即用的支持。这让我决定写这篇东西分享一下如何用Hutool在5分钟内搞定SM4的加解密并且灵活地在Base64和Hex这两种最常见的编码格式间切换。如果你也在找一种快速、可靠且代码简洁的SM4实现方案这篇实战记录应该能帮到你。SM4作为国家密码管理局认定的商用密码算法在金融、政务、物联网等领域应用越来越广。它的块大小是128位密钥长度也是128位安全性有保障。但在实际开发中我们拿到手的密钥可能是一串Hex字符串要加密的数据可能是文本而下游系统可能要求你提供Base64编码的密文。手动处理这些转换虽然不难但容易出错代码也显得臃肿。Hutool的价值就在于它用一个高度抽象的SmUtil类把算法实现、模式选择、填充方式以及最终的编码转换全部封装成了几行简单的方法调用。你不需要关心SM4内部复杂的轮函数运算只需要关注业务逻辑用什么密钥、加密什么数据、想要什么格式的输出。这对于追求开发效率和代码可维护性的项目来说是一个非常实用的选择。2. 核心思路与Hutool工具选型解析2.1 为什么是Hutool而不是手动实现或其它库面对SM4加密需求通常有几种路径一是完全手写算法实现二是使用Bouncy Castle这类重量级安全提供者三是寻找像Hutool这样更高层次的工具库。我选择Hutool基于几个很实际的考虑。首先完全手写实现SM4算法对于大多数业务开发场景来说性价比极低。你需要透彻理解算法原理处理各种边界情况并且自己编写大量的测试用例来保证正确性和安全性这完全是密码学专家的工作范畴而非普通应用开发者的首要任务。其次虽然Bouncy CastleBC是Java生态中功能最强大的密码学库之一绝对权威且完整支持国密算法但它也存在一些“痛点”。BC的API设计相对底层和复杂初始化一个密码器Cipher需要一堆参数代码写起来冗长。更重要的是BC默认可能不在Java的标准运行环境中你需要手动添加依赖并注册安全提供者这会给项目部署和依赖管理带来额外的复杂度。而Hutool在底层实际上可以依赖BC如果存在的话来提供算法实现但它提供了一层极度简化的API。也就是说你享受了BC的可靠性和Hutool的便捷性同时依赖管理更加清晰——你只需要引入Hutool一个依赖它内部会处理好与BC的协作如果检测到BC存在。最后Hutool的SmUtil国密算法工具类设计得非常“傻瓜式”。它的方法名直白如sm4、encrypt、decrypt参数通常就是密钥和待处理数据并且它直接支持String、byte[]类型以及通过HexUtil和Base64类无缝进行编码转换。这种设计将开发者的心智负担降到了最低你几乎不需要查阅复杂的密码学标准文档就能完成工作。这对于需要快速原型开发、或者团队里开发人员密码学背景不一的情况尤为友好。2.2 项目核心依赖与环境准备要开始这个“5分钟实战”你的项目需要先引入Hutool的依赖。这里以Maven项目为例在你的pom.xml文件中添加以下依赖。我强烈建议使用最新的稳定版本因为Hutool社区活跃新版本通常会修复已知问题并带来性能优化。dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version !-- 请检查并使用最新版本 -- /dependency如果你的项目有严格的依赖大小控制也可以只引入加密模块hutool-crypto。但hutool-all是个“全家桶”包含了我们可能用到的其他工具类比如HexUtil、Base64对于演示和大多数项目来说更方便。注意关于Bouncy Castle的依赖Hutool从某个版本开始其hutool-crypto模块已经将Bouncy Castle的可选依赖bcprov-jdk15to18改为强制依赖。这意味着你引入Hutool-all或Hutool-crypto后Bouncy Castle库会自动被传递进来。无需你再手动声明BC依赖。你可以通过mvn dependency:tree命令查看确认。这种做法简化了配置确保了SM4等算法有可靠的底层实现。环境方面确保你的JDK版本在1.8及以上即可。Hutool 5.x对JDK 8有良好的兼容性。完成依赖添加后你就可以在代码中导入相关的类了。核心类主要是cn.hutool.crypto.SmUtil以及用于编码解码的cn.hutool.core.util.HexUtil和cn.hutool.core.codec.Base64。3. SM4加密核心细节与Hutool API详解3.1 理解SM4的密钥、模式与填充在使用Hutool之前哪怕我们不用关心算法内部也需要理解几个关键概念这能帮助我们在调用API时做出正确的选择。密钥KeySM4算法使用一个固定的128位16字节密钥。这意味着无论你是用字符串还是Hex字符串表示最终它都必须能转换为16个字节。Hutool的API很灵活它允许你直接传入一个16字节的byte[]数组也可以传入一个长度为32的Hex字符串因为每个字节用两个十六进制字符表示或者一个长度合适的字符串它会按指定字符集转为字节。Hutool内部会帮你完成这些转换和长度校验。工作模式Mode块密码算法不能直接加密任意长度的数据需要模式。最常见的是ECB电子密码本和CBC密码块链接。ECB模式最简单的模式将数据分成块每块独立加密。缺点非常明显相同的明文块会生成相同的密文块对于有规律的数据密文也会呈现规律安全性较低。一般不推荐用于加密有意义的数据序列。CBC模式需要一个额外的“初始化向量”IVInitialization Vector。每个明文块在加密前会先与前一个密文块第一块与IV进行异或操作。这样即使明文相同只要IV不同产生的密文就完全不同安全性远高于ECB。IV不需要保密但必须不可预测通常随机生成且每次加密最好使用不同的IV。填充Padding由于SM4是128位16字节块加密当明文长度不是16字节的整数倍时就需要填充到整块。最常用的是PKCS#7 Padding在PKCS#5中也常用。Hutool默认使用的就是这种填充方式。Hutool的SmUtil.sm4()方法默认创建的是一个使用ECB模式、PKCS7Padding填充的SM4密码器。如果你需要更安全的CBC模式需要显式地通过SmUtil.sm4(Mode, Padding)来指定并传入IV。3.2 Hutool SM4 API 核心方法一览Hutool的SmUtil类提供了静态工厂方法来创建SM4加密器对象主要方法是sm4()。获得SM4对象后就可以调用其加密解密方法。最常用的方法签名如下创建加密器SmUtil.sm4(): 创建默认ECB/PKCS7Padding的SM4加密器。SmUtil.sm4(byte[] key): 用指定密钥字节数组创建默认加密器。SmUtil.sm4(String key): 用指定密钥字符串按UTF-8编码转为字节创建默认加密器。SmUtil.sm4(Mode mode, Padding padding): 创建指定模式和填充的加密器之后需要用.setKey(key)和.setIv(iv)如果是CBC等模式来设置参数。加密方法以SM4对象调用encrypt(byte[] data): 加密字节数组返回密文字节数组。encrypt(InputStream data, OutputStream out, boolean isClose): 加密流适合大文件。encryptHex(byte[] data)/encryptHex(String data): 加密数据并将结果密文字节数组转换为Hex十六进制字符串返回。encryptBase64(byte[] data)/encryptBase64(String data): 加密数据并将结果密文字节数组转换为Base64字符串返回。解密方法以SM4对象调用decrypt(byte[] data): 解密密文字节数组返回明文字节数组。decrypt(InputStream data, OutputStream out, boolean isClose): 解密流。decryptStr(byte[] data, Charset charset): 解密密文字节数组并将结果明文字节数组按指定字符集转为字符串。这是最常用的解密字符串的方法。对于Hex或Base64格式的密文通常需要先将其解码为byte[]再调用decrypt或decryptStr。Hutool也提供了decryptFromHex和decryptFromBase64的快捷方法具体版本需查看API。一个重要的设计思想Hutool将“加密/解密”和“编码/解码”进行了分离。encrypt方法核心产出是byte[]。encryptHex和encryptBase64是在加密的基础上自动帮你做了一层编码转换。同样解密时你需要先确保传入decrypt方法的是原始的密文字节数组。如果密文是Hex或Base64字符串你需要先将其解码回byte[]。这种设计清晰且符合逻辑。4. 实战演练5分钟完成Base64与Hex加密下面我们通过几个具体的代码示例来演示如何完成从密钥准备、数据加密到结果解码的全过程。我会分别展示ECB模式和更推荐的CBC模式。4.1 场景一使用ECB模式进行Hex格式加密解密假设我们有一个16字节的密钥以Hex字符串形式给出“0123456789abcdeffedcba9876543210”32个字符。我们要加密字符串“Hello, SM4!”并输出Hex格式的密文。import cn.hutool.core.util.HexUtil; import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.symmetric.SM4; public class Sm4ECBHexDemo { public static void main(String[] args) { // 1. 准备密钥 (32位Hex字符串对应16字节) String hexKey 0123456789abcdeffedcba9876543210; // 要加密的明文 String plainText Hello, SM4!; // 2. 创建SM4加密器 (默认ECB模式PKCS7Padding) // 这里直接传入Hex密钥字符串Hutool会自动识别并解码 SM4 sm4 SmUtil.sm4(hexKey); // 3. 加密并直接输出Hex字符串 String cipherTextHex sm4.encryptHex(plainText); System.out.println(密钥(Hex): hexKey); System.out.println(明文: plainText); System.out.println(密文(Hex): cipherTextHex); // 输出示例密文(Hex): 9a4d7b2c5e8f1a3d0b6c9e2f4a7d1b5c // 4. 解密Hex格式的密文 // 方法一使用decryptFromHex快捷方法如果API支持 // String decryptedText sm4.decryptStrFromHex(cipherTextHex); // 方法二通用方法先Hex解码再解密字符串 byte[] cipherBytes HexUtil.decodeHex(cipherTextHex); String decryptedText sm4.decryptStr(cipherBytes); System.out.println(解密后明文: decryptedText); // 输出: Hello, SM4! } }代码解读与注意事项第2步中SmUtil.sm4(hexKey)非常关键。它内部会调用HexUtil.decodeHex将32位的Hex字符串转换为16字节的密钥byte[]。如果你传入的Hex密钥长度不是32位或者包含非Hex字符0-9, a-f, A-F则会抛出异常。第3步的encryptHex方法内部完成了加密 - 得到byte[] - 转换为Hex字符串的整个过程。第4步的解密演示了更通用的流程先将Hex密文字符串解码回原始的密文字节数组cipherBytes再用sm4对象的decryptStr方法解密并转为字符串。有些Hutool版本提供了decryptFromHex这样的便捷方法但为了代码的兼容性和清晰理解原理掌握HexUtil.decodeHexdecryptStr的组合是更稳妥的。重要警告本例使用了ECB模式。你可以尝试加密一段有重复模式的较长文本然后观察其Hex密文很可能会发现重复的片段这印证了ECB模式的安全性缺陷。因此除非有特殊兼容性要求否则在生产环境中应避免使用ECB模式。4.2 场景二使用CBC模式进行Base64格式加密解密为了提高安全性我们使用CBC模式。CBC模式需要一个初始化向量IV它也是一个128位16字节的数据块。我们同样用Hex字符串来定义IV。import cn.hutool.core.codec.Base64; import cn.hutool.core.util.HexUtil; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.symmetric.SM4; public class Sm4CBCBase64Demo { public static void main(String[] args) { // 1. 准备密钥和IV均为32位Hex字符串 String hexKey 0123456789abcdeffedcba9876543210; String hexIv 1234567890abcdef1234567890abcdef; // 示例IV实践中应随机生成 // 2. 创建SM4加密器指定CBC模式和PKCS7Padding // 先创建对象再分别设置密钥和IV SM4 sm4 SmUtil.sm4(Mode.CBC, Padding.PKCS7Padding); sm4.setKey(HexUtil.decodeHex(hexKey)); // 设置密钥字节数组 sm4.setIv(HexUtil.decodeHex(hexIv)); // 设置IV字节数组 // 3. 加密并输出Base64字符串 String plainText 这是一段需要加密的敏感数据比如用户身份证号。; String cipherTextBase64 sm4.encryptBase64(plainText, UTF-8); // 指定明文编码 System.out.println(密钥(Hex): hexKey); System.out.println(IV(Hex): hexIv); System.out.println(明文: plainText); System.out.println(密文(Base64): cipherTextBase64); // 输出示例密文(Base64): t7K8sL2yvLC9sr2wvbK9sL2y... (很长一串) // 4. 解密Base64格式的密文 // 通用方法先Base64解码再解密字符串 byte[] cipherBytes Base64.decode(cipherTextBase64); String decryptedText sm4.decryptStr(cipherBytes); System.out.println(解密后明文: decryptedText); } }代码解读与注意事项第2步展示了如何创建CBC模式的SM4加密器。Mode.CBC和Padding.PKCS7Padding是Hutool定义的枚举常量。创建后必须通过setKey和setIv方法传入字节数组形式的密钥和IV。这里我们使用HexUtil.decodeHex进行转换。第3步的encryptBase64方法第二个参数可以指定明文字符串的编码通常用“UTF-8”。第4步的解密同样遵循“先解码后解密”的原则。使用Base64.decode将Base64密文还原为密文字节数组。关于IV的生成示例中的IV是硬编码的这仅用于演示。在实际应用中IV必须是随机且不可预测的。通常的做法是在加密时使用安全的随机数生成器如SecureRandom生成一个16字节的随机IV然后将这个IV可以转换为Hex或Base64和密文一起存储或传输给接收方。接收方解密时需要使用相同的IV。Hutool本身没有提供自动生成并拼接IV的方法需要开发者自己处理这个逻辑。一个常见的模式是最终传输数据 Base64(IV字节数组) “:” Base64(密文字节数组)。4.3 场景三处理字节数组与文件流加密有时我们需要加密的不是字符串而是二进制数据如图片、文档或直接从文件流中读取数据。Hutool的API同样支持。import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.HexUtil; import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.symmetric.SM4; import java.io.*; public class Sm4BytesAndStreamDemo { public static void main(String[] args) throws IOException { String hexKey 0123456789abcdeffedcba9876543210; SM4 sm4 SmUtil.sm4(hexKey); // 1. 加密字节数组 byte[] originalBytes Some binary data \0\1\2.getBytes(UTF-8); byte[] encryptedBytes sm4.encrypt(originalBytes); String hexOfEncrypted HexUtil.encodeHexStr(encryptedBytes); System.out.println(字节数组密文(Hex): hexOfEncrypted); // 2. 解密回字节数组 byte[] decryptedBytes sm4.decrypt(encryptedBytes); System.out.println(解密后字节数组是否相等: java.util.Arrays.equals(originalBytes, decryptedBytes)); // 3. 文件流加密/解密 (示例加密一个文件) File sourceFile new File(test.txt); File encryptedFile new File(test.txt.encrypted); File decryptedFile new File(test.txt.decrypted); // 写入一个测试文件 FileUtil.writeString(This is a secret file content., sourceFile, UTF-8); try (InputStream in FileUtil.getInputStream(sourceFile); OutputStream out FileUtil.getOutputStream(encryptedFile)) { // 加密流并自动关闭输入输出流第三个参数为true sm4.encrypt(in, out, true); } System.out.println(文件加密完成: encryptedFile.getPath()); // 解密文件流 try (InputStream inEnc FileUtil.getInputStream(encryptedFile); OutputStream outDec FileUtil.getOutputStream(decryptedFile)) { sm4.decrypt(inEnc, outDec, true); } System.out.println(文件解密完成: decryptedFile.getPath()); System.out.println(解密文件内容: FileUtil.readString(decryptedFile, UTF-8)); } }代码解读与注意事项对于字节数组直接使用encrypt(byte[])和decrypt(byte[])方法它们返回的也是字节数组。你可以用HexUtil.encodeHexStr或Base64.encode将其转换为字符串以便查看或传输。对于大文件使用流操作encrypt(InputStream, OutputStream, boolean)可以避免将整个文件加载到内存中对于内存敏感的应用场景非常重要。第三个参数isClose表示是否在加密/解密完成后自动关闭流通常设为true以简化资源管理。文件操作示例中使用了Hutool的FileUtil工具类来简化IO这同样是Hutool工具集的一部分让代码更简洁。5. 常见问题、排查技巧与性能考量在实际集成和使用过程中你可能会遇到一些典型问题。下面我总结了一份排查清单和对应的解决思路。5.1 问题排查速查表问题现象可能原因排查步骤与解决方案抛出InvalidKeyException或 “非法密钥”错误1. 密钥长度不是128位16字节。2. 密钥Hex字符串长度不是32位或包含非法字符。3. 密钥字符串在转换为字节时编码不一致。1. 确认密钥源。如果是字符串确保其UTF-8编码的字节长度为16。“my16bytekey!!”是16字节“我的密钥”的字节长度可能不是16。2. 打印密钥字节数组长度System.out.println(keyBytes.length);必须为16。3. 对于Hex密钥使用HexUtil.decodeHex前确保字符串是纯Hex0-9, a-f, A-F且长度为32。解密失败抛出BadPaddingException1.密钥错误加密和解密使用的密钥不一致。2.IV错误CBC模式加密和解密使用的IV不一致。3.密文被篡改或编码损坏传输过程中密文或IV发生了变化。4.模式或填充不匹配加密用CBC解密用ECB或者填充方式不同。1.最可能的原因。仔细核对加密端和解密端的密钥和IV是否完全一致包括类型字符串/Hex/Base64和转换过程。2. 确保CBC模式下IV被正确传递和使用。比较加密和解密时sm4.getIv()的值。3. 检查密文字符串在传输过程中是否被URLDecode、Trim等操作意外修改。Base64密文末尾的填充符很重要。4. 确认双方使用的Mode和Padding枚举值完全相同。加密后的Hex/Base64字符串自己无法解密1.编码/解码环节出错加密后做了编码解密前没有做对应的解码。2.字符串编码问题加解密时使用的字符集不一致。1.牢记流程加密明文 - encrypt - 密文byte[] - encodeHex/Base64 - 密文字符串。解密密文字符串 - decodeHex/Base64 - 密文byte[] - decrypt - 明文。检查解密前是否漏了HexUtil.decodeHex或Base64.decode。2. 在encrypt(String, Charset)和decryptStr(byte[], Charset)中明确指定字符集如StandardCharsets.UTF_8。与其他系统如C/Python加解密结果不一致1.密钥/IV表示不同对方可能将字符串密钥直接作为字节使用而Hutool可能按UTF-8编码。2.模式/填充不同对方可能使用不同的模式如CFB或填充如ZeroPadding。3.数据格式不同对方可能对明文进行了额外的处理如添加头尾。1.对齐基础参数这是跨语言密码学交互最大的难点。必须书面确认并统一密钥长度16字节、密钥来源字符串及其编码、Hex字符串、IV来源、工作模式CBC、填充方式PKCS7。2. 进行逐字节对比。先用一个简单的、双方已知的密钥和IV如全零加密一个短明文如“AA”然后对比中间每一步的结果密钥字节数组、IV字节数组、加密前的明文字节数组、加密后的密文字节数组。通常能快速定位分歧点。3. 考虑使用标准的测试向量进行验证。5.2 性能优化与最佳实践建议重用SM4对象SmUtil.sm4()创建SM4对象的过程涉及一些初始化工作。如果你的应用需要频繁使用同一个密钥进行加解密应该将这个SM4对象创建一次并缓存起来例如放在Spring的Component中避免每次调用都重新创建。这对于高并发服务尤为重要。IV的管理CBC模式如前所述IV必须随机且不可重复。一个最佳实践是每次加密都生成一个新的随机IV使用SecureRandom然后将这个IV以Hex或Base64格式和加密后的密文拼接在一起再发送或存储。解密时先分离出IV和密文部分。例如// 加密端 SecureRandom random new SecureRandom(); byte[] ivBytes new byte[16]; random.nextBytes(ivBytes); sm4.setIv(ivBytes); byte[] cipherBytes sm4.encrypt(data); String result Base64.encode(ivBytes) : Base64.encode(cipherBytes); // 用冒号分隔 // 解密端 String[] parts receivedResult.split(:); byte[] ivForDecrypt Base64.decode(parts[0]); byte[] cipherBytesForDecrypt Base64.decode(parts[1]); sm4Decryptor.setIv(ivForDecrypt); byte[] decryptedBytes sm4Decryptor.decrypt(cipherBytesForDecrypt);错误处理加解密操作可能抛出各种异常CryptoException,InvalidKeyException,BadPaddingException等。在生产代码中务必进行妥善的异常捕获和处理记录详细的日志注意不要记录敏感的密钥或明文数据并返回友好的业务错误信息而不是将底层密码学异常直接暴露给用户。依赖管理虽然Hutool-all很方便但如果对包大小有极致要求可以只引入hutool-crypto。使用mvn dependency:tree命令检查是否引入了预期的Bouncy Castle版本避免版本冲突。安全提醒算法和工具库是安全的基石但密钥管理才是安全的核心。绝对不要将密钥硬编码在源代码中或提交到版本控制系统。应该使用安全的密钥管理系统如HashiCorp Vault、AWS KMS、或云原生的Secret管理服务在运行时通过环境变量、配置中心或安全接口动态获取密钥。6. 完整工具类封装示例根据上面的实践我们可以封装一个更健壮、易用的SM4工具类集成密钥校验、异常处理、以及自动的IV处理针对CBC模式。import cn.hutool.core.codec.Base64; import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.CryptoException; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.symmetric.SM4; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; /** * SM4加解密工具类 (CBC模式PKCS7Padding) */ public class Sm4CbcUtils { private static final int KEY_LENGTH_BYTES 16; // SM4密钥长度 16字节 private static final int IV_LENGTH_BYTES 16; // CBC IV长度 16字节 private static final String DELIMITER :; // IV与密文的分隔符 /** * 从Hex字符串生成SM4密钥对象 * param hexKey 32位Hex字符串 * return SecretKeySpec * throws IllegalArgumentException 密钥格式错误 */ public static SecretKeySpec generateKeyFromHex(String hexKey) { if (StrUtil.isBlank(hexKey) || hexKey.length() ! 32) { throw new IllegalArgumentException(SM4密钥必须为32位Hex字符串); } byte[] keyBytes; try { keyBytes HexUtil.decodeHex(hexKey); } catch (Exception e) { throw new IllegalArgumentException(SM4密钥Hex字符串格式错误, e); } if (keyBytes.length ! KEY_LENGTH_BYTES) { throw new IllegalArgumentException(SM4密钥长度必须为16字节); } return new SecretKeySpec(keyBytes, SM4); } /** * 加密文本返回Base64编码的字符串格式为 Base64(IV):Base64(CipherText) * param plainText 明文 * param hexKey 32位Hex密钥 * return 拼接后的密文字符串 */ public static String encryptToBase64WithIv(String plainText, String hexKey) { try { SecretKeySpec keySpec generateKeyFromHex(hexKey); // 生成随机IV SecureRandom random new SecureRandom(); byte[] iv new byte[IV_LENGTH_BYTES]; random.nextBytes(iv); SM4 sm4 SmUtil.sm4(Mode.CBC, Padding.PKCS7Padding); sm4.setKey(keySpec.getEncoded()); sm4.setIv(iv); byte[] cipherBytes sm4.encrypt(plainText, StandardCharsets.UTF_8); // 拼接: Base64(IV) : Base64(Cipher) return Base64.encode(iv) DELIMITER Base64.encode(cipherBytes); } catch (Exception e) { throw new CryptoException(SM4加密失败, e); } } /** * 解密由 encryptToBase64WithIv 方法加密的字符串 * param encryptedTextWithIv 加密返回的字符串 Base64(IV):Base64(CipherText) * param hexKey 32位Hex密钥 * return 明文 */ public static String decryptFromBase64WithIv(String encryptedTextWithIv, String hexKey) { try { if (StrUtil.isBlank(encryptedTextWithIv) || !encryptedTextWithIv.contains(DELIMITER)) { throw new IllegalArgumentException(密文格式错误应为 Base64(IV):Base64(CipherText)); } String[] parts encryptedTextWithIv.split(DELIMITER, 2); byte[] iv Base64.decode(parts[0]); byte[] cipherBytes Base64.decode(parts[1]); SecretKeySpec keySpec generateKeyFromHex(hexKey); SM4 sm4 SmUtil.sm4(Mode.CBC, Padding.PKCS7Padding); sm4.setKey(keySpec.getEncoded()); sm4.setIv(iv); return sm4.decryptStr(cipherBytes); } catch (IllegalArgumentException e) { throw e; // 参数错误直接抛出 } catch (Exception e) { throw new CryptoException(SM4解密失败, e); } } // 可以类似地添加 encryptToHexWithIv / decryptFromHexWithIv 方法 // 以及使用固定IV的简化方法不推荐生产环境使用 /** * 使用固定IV加密仅用于测试或特定场景 */ public static String encryptWithFixedIv(String plainText, String hexKey, String hexIv) { try { SecretKeySpec keySpec generateKeyFromHex(hexKey); byte[] ivBytes HexUtil.decodeHex(hexIv); SM4 sm4 SmUtil.sm4(Mode.CBC, Padding.PKCS7Padding); sm4.setKey(keySpec.getEncoded()); sm4.setIv(ivBytes); byte[] cipherBytes sm4.encrypt(plainText, StandardCharsets.UTF_8); return Base64.encode(cipherBytes); // 只返回密文IV由调用方管理 } catch (Exception e) { throw new CryptoException(SM4加密失败, e); } } }这个工具类提供了几个关键改进密钥验证在generateKeyFromHex中严格检查Hex密钥的格式和长度。自动IV管理encryptToBase64WithIv方法在加密时自动生成随机IV并将其与密文一起用Base64编码后用冒号拼接返回。解密方法decryptFromBase64WithIv能自动解析这种格式。统一异常处理将底层的各种异常包装成业务友好的CryptoException并记录了根本原因。字符集固定明确使用StandardCharsets.UTF_8避免因平台默认编码不同导致的问题。你可以根据项目需求在此基础上进一步扩展比如增加Hex格式的对应方法、增加文件流处理的封装等。封装成这样的工具类后业务代码中的调用就变得非常简洁和安全了。