
1. 项目概述为什么我们需要关注SM4最近在做一个需要数据安全传输的项目和团队讨论加密方案时有人提了AES也有人提了DES。我顺口问了句“咱们不考虑一下国密算法吗” 结果现场安静了几秒然后有人问“国密是啥稳定吗有现成的库吗” 这让我意识到尽管国密算法SM系列在金融、政务等领域已是强制标准但在更广泛的Java开发者社区里它的认知度和使用经验仍然像一层窗户纸需要有人去捅破。SM4就是这层纸里很关键的一张。它不是一个高高在上的概念而是一个我们每天可能都在间接使用的算法——比如你在银行APP里转账、在政务网站提交材料后台很可能就在用它保护你的数据。作为对称加密算法SM4和AES是“同行”都用于加密大量数据但它的“国籍”和设计理念有所不同。在自主可控越来越被重视的当下无论是为了满足特定行业的合规性要求还是为产品增加一份技术上的“备选方案”和“安全感”掌握SM4的Java实现都是一项很实用的技能。这篇文章我就从一个Java老兵的角度带你从SM4的基础原理开始一步步拆解最后封装成一个拿来即用、生产级可靠的SM4加解密工具类。我们会避开纯理论的枯燥聚焦在“如何用Java把它实现出来、用起来、并且用好”。过程中遇到的坑、参数怎么选、性能如何调优这些实战经验我都会毫无保留地分享出来。无论你是正在对接需要国密算法的第三方平台还是单纯想拓宽自己的技术储备这篇内容都能给你一条清晰的路径。2. SM4算法核心原理快速解读在动手写代码之前我们得先搞清楚SM4到底是怎么工作的。不用担心我不会堆砌复杂的数学公式而是用程序员能懂的方式把它和熟悉的老朋友AES做个对比这样理解起来更快。2.1 对称加密与分组密码SM4的基本定位首先SM4是一种分组对称加密算法。这几个词拆开看对称加密加密和解密使用同一把密钥。就像你用同一把钥匙锁门和开门。这把密钥的保管和分发是关键但加解密过程本身效率很高。SM4和AES、DES都属于这一类。分组密码它不会一个字节一个字节地加密而是把明文数据切成固定大小的“块”Block然后对每个块单独进行加密。SM4的块大小是128位16字节。这和AES的块大小是一样的所以它们在处理数据分割和填充Padding时逻辑是相通的。2.2 核心流程轮函数与密钥扩展SM4的加密过程可以想象成一个复杂的“搅拌机”。它把16字节的明文块和一把32字节256位的原始主密钥扔进去经过32轮完全相同的“搅拌”操作称为轮函数最终输出16字节的密文块。解密过程则是这个搅拌机的逆过程。这里有两个关键点需要理解轮函数F函数这是每一轮“搅拌”的核心。它接收上一轮的中间结果和本轮的子密钥进行一系列操作非线性变换S盒替换提供混淆、线性变换L函数提供扩散、再和子密钥进行异或。正是这32轮重复但数据不同的操作使得输入数据的微小变化会引起密文的巨大差异确保了安全性。密钥扩展你提供的原始主密钥是256位的但每一轮搅拌需要一个32位4字节的“子密钥”。所以算法第一步就是通过一个固定的密钥扩展算法把这256位的主密钥“摊煎饼”一样地生成32个32位的子密钥rk[0]到rk[31]。加密和解密使用的子密钥顺序是相反的加密用rk[0], rk[1], ..., rk[31]解密则用rk[31], rk[30], ..., rk[0]。这是SM4加解密能使用同一套结构的关键。注意很多初学者会混淆密钥长度。SM4的有效密钥长度是128位。虽然算法标准中描述了密钥长度为128位但一些库如Bouncy Castle的API中可能会使用256位的主密钥参数其内部会通过密钥扩展算法推导出实际用于加密的128位密钥材料。在实践时务必以你所使用库的官方文档为准。通常我们直接提供一个16字节128位的字节数组作为密钥即可。2.3 SM4 vs AES一个简单的对比了解差异有助于我们做出正确选择。你可以把SM4看作AES的一个“中国表亲”。特性SM4 (国密)AES (高级加密标准)诞生中国商用密码算法2012年成为国家标准比利时设计2001年被NIST选定为美国标准密钥长度固定128位可选128、192、256位分组大小固定128位固定128位加密轮数固定32轮根据密钥长度10轮(128位)、12轮(192位)、14轮(256位)设计结构基于Feistel网络类似DES基于置换-置换网络SPN核心优势自主可控符合国内特定行业金融、政务安全合规要求国际通用经过全球最广泛的分析和验证硬件优化支持极好性能软件实现上两者在现代CPU上性能接近。AES因有专用指令集AES-NI在支持它的CPU上性能有数量级优势。选择建议合规驱动如果你的项目用于国内金融、电子政务、关键基础设施等领域必须支持甚至强制使用SM4。性能驱动如果追求极限性能且运行环境普遍支持AES-NI绝大多数现代服务器和PC都支持AES是更优选择。兼容性驱动如果与国际系统对接AES是通用语言。技术储备作为开发者两者都应了解。在很多场景下系统需要同时支持两者以应对不同需求。理解了这些我们就知道在Java里实现SM4本质上就是按照上述规则去实现这个“128位分组、32轮搅拌、密钥逆序使用”的过程。幸运的是我们不需要从零开始造轮子。3. 环境准备与核心库选型在Java世界里我们很少需要从零实现一个密码学算法这不仅容易出错而且在安全性上也无法保证。使用成熟、经过审计的密码学库是唯一正确的选择。对于国密算法目前最主流、最可靠的选择就是Bouncy Castle。3.1 为什么是Bouncy CastleBouncy Castle是一个开源的、功能强大的密码学库提供了Java和C#版本。它几乎是Java领域处理非标准或最新密码学算法的“瑞士军刀”。其优势在于对国密算法的完整支持Bouncy Castle很早就提供了对SM2、SM3、SM4等国密算法的实现并且持续维护。作为JCE提供者它可以无缝集成到Java标准的Javax.Crypto框架中。这意味着你可以使用熟悉的Cipher、KeyGenerator等标准API来操作SM4只是指定算法名时换成SM4底层实现由Bouncy Castle提供。活跃的社区和广泛验证经过多年众多商业和开源项目的使用其代码相对稳定可靠。3.2 项目依赖引入如果你的项目使用Maven在pom.xml中添加以下依赖即可dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15to18/artifactId !-- 根据你的JDK版本选择15to18适用于JDK 1.5-1.8及更高兼容版本 -- version1.78/version !-- 请使用最新稳定版本 -- /dependency如果你使用Gradleimplementation org.bouncycastle:bcprov-jdk15to18:1.78版本选择注意bcprov-jdk15to18是一个兼容性很广的版本。Bouncy Castle还有针对更新JDK的bcprov-jdk18on等。选择时以官方仓库Maven Central的最新稳定版文档为准。引入后我们需要在代码中将其注册为JCE的安全提供者。3.3 初始化安全提供者这一步至关重要它告诉Java“嘿当我要用SM4算法的时候请去找Bouncy Castle要实现。” 通常我们在程序启动初期或者工具类静态初始化块里执行。import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class Sm4Util { static { // 防止重复注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } // ... 后续工具类代码 }实操心得注册提供者的代码放在静态块中能确保类加载时只执行一次。一定要检查是否已注册避免在热部署或多次加载时抛出ProviderAlreadyExistsException虽然这个异常通常无害但保持代码整洁是好事。3.4 算法名称与模式、填充在JCE的Cipher类中我们通过一个字符串来指定算法格式通常是算法/模式/填充。算法对我们来说就是SM4。模式Mode指如何用分组密码加密超过一个块的数据。常见的有ECB(Electronic Codebook)电子密码本模式。最不推荐相同的明文块会产生相同的密文块不能很好地隐藏数据模式安全性低。除非万不得已如对接某些老旧且定死的系统否则不要使用。CBC(Cipher Block Chaining)密码分组链接模式。最常用。它需要一个初始化向量IV来增加随机性相同的明文每次加密结果都不同安全性好。这是默认推荐的选择。CTR(Counter)计数器模式。它可以将分组密码转换为流密码可以并行加密在某些场景下性能有优势。填充Padding因为分组密码需要处理固定大小的块当明文长度不是块大小的整数倍时就需要填充。常见的有PKCS5Padding或PKCS7Padding在Java中对于128位16字节的块这两者通常是等价的。它是最常用的填充方式。NoPadding无填充。使用此模式时你必须确保待加密数据的长度恰好是16字节的整数倍否则会抛出异常。因此一个完整、推荐的算法指定字符串是SM4/CBC/PKCS5Padding。这表示使用SM4算法CBC模式并采用PKCS5填充。4. 实战SM4工具类设计与实现理论准备就绪我们现在来打造一个健壮、易用的SM4工具类。这个工具类需要处理加密、解密并妥善管理密钥和IV。4.1 密钥与IV的生成与管理密钥是加密的根基必须安全生成和保管。import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; public class Sm4Util { // ... 静态初始化块 ... /** * 生成一个随机的SM4密钥128位 * return 生成的SecretKey对象 */ public static SecretKey generateKey() throws NoSuchAlgorithmException { // 指定算法为SM4 KeyGenerator keyGenerator KeyGenerator.getInstance(SM4); // 初始化密钥生成器使用强随机数源 keyGenerator.init(128, new SecureRandom()); // SM4密钥长度固定128位 return keyGenerator.generateKey(); } /** * 将SecretKey对象转换为Base64编码的字符串便于存储或传输 */ public static String keyToBase64(SecretKey secretKey) { return Base64.getEncoder().encodeToString(secretKey.getEncoded()); } /** * 从Base64字符串还原SecretKey对象 */ public static SecretKey keyFromBase64(String base64Key) { byte[] keyBytes Base64.getDecoder().decode(base64Key); // 使用SecretKeySpec来重建密钥指定算法为SM4 return new javax.crypto.spec.SecretKeySpec(keyBytes, SM4); } /** * 生成一个随机的初始化向量IV16字节 * CBC模式必须使用IV且每次加密最好使用不同的IV */ public static IvParameterSpec generateIv() { byte[] iv new byte[16]; // SM4块大小是16字节IV长度需与块大小一致 new SecureRandom().nextBytes(iv); return new IvParameterSpec(iv); } /** * 将IV转换为Base64字符串 */ public static String ivToBase64(IvParameterSpec iv) { return Base64.getEncoder().encodeToString(iv.getIV()); } /** * 从Base64字符串还原IvParameterSpec对象 */ public static IvParameterSpec ivFromBase64(String base64Iv) { byte[] ivBytes Base64.getDecoder().decode(base64Iv); return new IvParameterSpec(ivBytes); } }注意事项密钥保管生成的密钥Base64字符串需要妥善保管例如放入配置中心如Apollo、Nacos、环境变量或硬件安全模块HSM中。绝对不要硬编码在源代码里或提交到版本库。IV的作用IV不需要像密钥一样永久保密但它对于同一密钥下的加密唯一性至关重要。通常IV可以随密文一起存储或传输例如拼接在密文前面。解密时再用同样的IV即可。SecureRandom务必使用SecureRandom来生成密钥和IV而不是普通的Random类以确保密码学意义上的强随机性。4.2 核心加密方法实现我们实现一个通用的加密方法支持字符串和字节数组的输入。import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; public class Sm4Util { // ... 之前的代码 ... // 定义常用的算法模式常量 public static final String ALGORITHM_CBC_PKCS5 SM4/CBC/PKCS5Padding; public static final String ALGORITHM_ECB_PKCS5 SM4/ECB/PKCS5Padding; // 不推荐仅用于兼容 /** * SM4加密通用方法 * param data 明文数据字节数组 * param key 密钥字节数组长度必须为16字节 * param iv 初始化向量字节数组长度必须为16字节ECB模式可传null * param algorithm 算法/模式/填充字符串如 SM4/CBC/PKCS5Padding * return 密文字节数组 */ public static byte[] encrypt(byte[] data, byte[] key, byte[] iv, String algorithm) throws Exception { // 参数校验 if (key null || key.length ! 16) { throw new IllegalArgumentException(SM4密钥必须为16字节128位); } if (iv null algorithm.toUpperCase().contains(CBC)) { throw new IllegalArgumentException(CBC模式必须提供初始化向量(IV)); } if (iv ! null iv.length ! 16) { throw new IllegalArgumentException(IV必须为16字节); } // 1. 创建Cipher实例并指定算法 Cipher cipher Cipher.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME); // 2. 根据模式初始化Cipher加密模式 SecretKeySpec secretKeySpec new SecretKeySpec(key, SM4); if (iv ! null) { IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec); } else { // ECB模式 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); } // 3. 执行加密 return cipher.doFinal(data); } /** * 便捷方法加密字符串返回Base64编码的密文 * param plainText 明文字符串 * param keyBase64 Base64编码的密钥字符串 * param ivBase64 Base64编码的IV字符串CBC模式必需 * return Base64编码的密文字符串 */ public static String encryptText(String plainText, String keyBase64, String ivBase64) throws Exception { byte[] key Base64.getDecoder().decode(keyBase64); byte[] iv (ivBase64 ! null !ivBase64.isEmpty()) ? Base64.getDecoder().decode(ivBase64) : null; byte[] data plainText.getBytes(StandardCharsets.UTF_8); byte[] encrypted encrypt(data, key, iv, ALGORITHM_CBC_PKCS5); // 默认使用CBC return Base64.getEncoder().encodeToString(encrypted); } /** * 更便捷的方法生成随机IV并返回“IV:密文”格式的字符串便于存储 * param plainText 明文 * param keyBase64 密钥 * return 格式为 Base64(IV):Base64(CipherText) 的字符串 */ public static String encryptTextWithIv(String plainText, String keyBase64) throws Exception { // 生成随机IV IvParameterSpec ivSpec generateIv(); String ivBase64 ivToBase64(ivSpec); // 加密 String cipherTextBase64 encryptText(plainText, keyBase64, ivBase64); // 组合返回用特定分隔符如冒号连接 return ivBase64 : cipherTextBase64; } }代码解析与心得参数校验在加密开始前校验密钥和IV的长度这是一个好习惯能提前暴露问题避免在cipher.init()时得到晦涩的异常。Provider指定Cipher.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME)这行代码明确指定使用Bouncy Castle提供者。虽然注册后有时不指定也能工作但显式指定可以避免因环境中有多个提供者而导致的意外行为。异常处理方法声明了throws Exception在实际工具类中你可能希望定义更具体的自定义异常如Sm4EncryptException来包装这些底层异常给调用方更清晰的错误信息。IV处理encryptTextWithIv方法演示了一种常见的IV处理模式每次加密生成一个随机IV并将其和密文捆绑在一起返回。解密时只需要按同样的分隔规则拆分即可。这样保证了即使同一明文、同一密钥每次加密结果也不同安全性更高。4.3 核心解密方法实现解密是加密的逆过程必须使用与加密时完全相同的算法、密钥和IV。public class Sm4Util { // ... 之前的代码 ... /** * SM4解密通用方法 * param encryptedData 密文数据字节数组 * param key 密钥字节数组长度必须为16字节 * param iv 初始化向量字节数组长度必须为16字节ECB模式可传null * param algorithm 算法/模式/填充字符串必须与加密时一致 * return 明文字节数组 */ public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv, String algorithm) throws Exception { // 参数校验同加密 if (key null || key.length ! 16) { throw new IllegalArgumentException(SM4密钥必须为16字节128位); } if (iv null algorithm.toUpperCase().contains(CBC)) { throw new IllegalArgumentException(CBC模式必须提供初始化向量(IV)); } if (iv ! null iv.length ! 16) { throw new IllegalArgumentException(IV必须为16字节); } Cipher cipher Cipher.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME); SecretKeySpec secretKeySpec new SecretKeySpec(key, SM4); if (iv ! null) { IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec); } else { cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); } return cipher.doFinal(encryptedData); } /** * 便捷方法解密Base64编码的密文字符串 * param cipherTextBase64 Base64编码的密文 * param keyBase64 Base64编码的密钥 * param ivBase64 Base64编码的IVCBC模式必需 * return 明文字符串 */ public static String decryptText(String cipherTextBase64, String keyBase64, String ivBase64) throws Exception { byte[] key Base64.getDecoder().decode(keyBase64); byte[] iv (ivBase64 ! null !ivBase64.isEmpty()) ? Base64.getDecoder().decode(ivBase64) : null; byte[] encryptedData Base64.getDecoder().decode(cipherTextBase64); byte[] decrypted decrypt(encryptedData, key, iv, ALGORITHM_CBC_PKCS5); return new String(decrypted, StandardCharsets.UTF_8); } /** * 解密由 encryptTextWithIv 方法加密的字符串 * param combinedText 格式为 Base64(IV):Base64(CipherText) 的字符串 * param keyBase64 Base64编码的密钥 * return 明文字符串 */ public static String decryptTextWithIv(String combinedText, String keyBase64) throws Exception { String[] parts combinedText.split(:, 2); // 只分割成两部分 if (parts.length ! 2) { throw new IllegalArgumentException(无效的加密文本格式应为 IV:CipherText); } String ivBase64 parts[0]; String cipherTextBase64 parts[1]; return decryptText(cipherTextBase64, keyBase64, ivBase64); } }解密方法几乎是加密方法的镜像。关键点在于一致性算法字符串、密钥、IV必须与加密时完全一致一个字节都不能差。4.4 完整工具类与使用示例将以上所有方法整合我们就得到了一个初步完整的Sm4Util工具类。下面是一个快速的使用示例public class Sm4Demo { public static void main(String[] args) { try { // 1. 生成密钥实际项目中密钥应从安全配置中读取 SecretKey secretKey Sm4Util.generateKey(); String base64Key Sm4Util.keyToBase64(secretKey); System.out.println(生成的SM4密钥(Base64): base64Key); // 2. 准备明文 String plainText 这是一段需要加密的敏感数据比如用户手机号13800138000; // 3. 加密使用自动生成IV的方式 String encryptedResult Sm4Util.encryptTextWithIv(plainText, base64Key); System.out.println(加密结果 (IV:CipherText): encryptedResult); // 4. 解密 String decryptedText Sm4Util.decryptTextWithIv(encryptedResult, base64Key); System.out.println(解密结果: decryptedText); System.out.println(解密是否成功: plainText.equals(decryptedText)); // 5. 演示固定IV的加密解密例如需要与使用固定IV的第三方系统对接 String fixedIvBase64 AAAAAAAAAAAAAAAAAAAAAA; // 示例IV全零实际中应使用随机值 String cipherText1 Sm4Util.encryptText(plainText, base64Key, fixedIvBase64); System.out.println(\n使用固定IV加密结果: cipherText1); String decryptedText1 Sm4Util.decryptText(cipherText1, base64Key, fixedIvBase64); System.out.println(使用固定IV解密结果: decryptedText1); } catch (Exception e) { e.printStackTrace(); } } }这个示例展示了工具类的基本用法。在实际项目中密钥管理、异常处理、日志记录等都需要更加完善。5. 进阶话题与生产环境考量一个能在生产环境跑起来的加密工具远不止调用API那么简单。下面这些点是区分“玩具代码”和“生产代码”的关键。5.1 性能优化与线程安全Cipher对象的创建成本Cipher.getInstance()是一个比较耗时的操作因为它涉及查找提供者、加载类等。在高并发场景下频繁创建和初始化Cipher对象会成为性能瓶颈。解决方案使用对象池。例如可以使用Apache Commons Pool或自己实现一个简单的ThreadLocal缓存。public class CipherPool { private static final ThreadLocalCipher cipherThreadLocal ThreadLocal.withInitial(() - { try { // 每个线程初始化一个Cipher实例 return Cipher.getInstance(Sm4Util.ALGORITHM_CBC_PKCS5, BouncyCastleProvider.PROVIDER_NAME); } catch (Exception e) { throw new RuntimeException(Failed to create Cipher instance, e); } }); public static Cipher getCipher() { return cipherThreadLocal.get(); } // 注意Cipher对象不是线程安全的但ThreadLocal保证了每个线程独享一个实例。 // 在使用前仍然需要调用 init() 方法进行正确的初始化设置模式、密钥、IV。 public static void initCipher(Cipher cipher, int opmode, SecretKey key, IvParameterSpec iv) throws Exception { cipher.init(opmode, key, iv); } }使用时先从池中获取Cipher对象然后用特定的密钥和IV初始化它再进行doFinal操作。注意Cipher对象在init()之后才能用于加解密且每次init()会重置其状态。5.2 异常处理与日志密码学操作可能抛出多种异常NoSuchAlgorithmException,NoSuchPaddingException,InvalidKeyException,InvalidAlgorithmParameterException,IllegalBlockSizeException,BadPaddingException等。不要吞掉异常在工具类内部可以捕获这些异常并转换为更有业务意义的运行时异常或自定义异常向上抛出。记录日志在关键步骤如密钥加载失败、加解密失败记录WARN或ERROR级别日志但切记不要将密钥、明文、密文等敏感信息记录到日志中。给调用方明确的错误信息例如BadPaddingException通常意味着密钥或IV错误或者是密文被篡改。你可以封装一个Sm4DecryptException并提示“解密失败请检查密钥或数据完整性”。5.3 与其他系统对接的兼容性问题这是实战中最容易踩坑的地方。你和另一个系统比如用C#、Python写的都用SM4但可能还是无法解密对方的数据。问题通常出在“细节”上编码问题密钥和明文在转换为字节数组时是否使用了相同的字符编码强烈建议全程使用UTF-8。IV的处理对方是把IV放在密文前面还是后面是用什么分隔的还是固定IV必须完全约定一致。上面的encryptTextWithIv方法是一种常见约定。Base64变种Base64有标准RFC 4648和URL安全等多种编码方式还有是否填充的区别。Java标准库的Base64.getEncoder()是标准的。如果对方用了其他变种你需要使用Base64.getUrlEncoder()或手动处理。填充模式对方用的是PKCS5Padding还是PKCS7Padding对于16字节块在Java/Bouncy Castle中它们通常等价但某些语言库可能有细微差别需要确认。密钥长度再次确认双方对密钥长度的理解是否一致128位/16字节。最佳实践在项目启动时与对接方共同制定一份《加解密对接文档》明确列出以上所有细节并编写一个简单的测试用例比如加密字符串“HelloWorld”互相验证确保双方实现结果一致。5.4 使用GMSSL等替代方案Bouncy Castle不是唯一选择。GMSSL是一个开源的、支持国密算法的密码工具箱它提供了命令行工具和C/Java/Python等语言的API。如果你需要更“原生”的国密支持或者项目环境对Bouncy Castle有依赖冲突可以考虑GMSSL的Java版本。使用GMSSL时你可能需要直接调用其JNI接口或特定的Jar包API风格可能与JCE标准不同需要重新学习。但对于深度集成国密、或有特定性能要求的场景它是一个值得评估的选项。不过对于大多数Java应用Bouncy Castle的JCE集成方式因其简单和标准化仍是首选。6. 常见问题排查与实战技巧即使有了工具类在实际集成和调试过程中你依然会遇到各种问题。下面是我总结的一些典型问题和解决方法。6.1 典型异常与解决方案速查表异常信息可能原因排查步骤与解决方案java.security.NoSuchAlgorithmException: SM4/CBC/PKCS5Padding1. Bouncy Castle提供者未成功注册。2. 依赖的Jar包未引入或版本冲突。1. 检查Security.addProvider代码是否执行打印Security.getProviders()查看。2. 检查pom.xml/gradle依赖使用mvn dependency:tree排查冲突。java.security.InvalidKeyException1. 密钥长度不正确不是16字节。2. 密钥字节数组内容错误如Base64解码出错。3. 密钥算法不匹配用AES的密钥给SM4用。1. 打印密钥字节数组长度确认是16。2. 检查密钥字符串的Base64解码过程确保无误。3. 确认密钥生成时指定的算法是“SM4”。javax.crypto.IllegalBlockSizeException1. 使用NoPadding时数据长度不是16字节的整数倍。2. 密文数据在传输或存储过程中被损坏或截断。1. 检查填充模式除非有特殊要求否则使用PKCS5Padding。2. 确保密文完整传输检查网络或存储过程。javax.crypto.BadPaddingException这是最常见的解密错误。1. 解密密钥与加密密钥不一致。2. 解密IV与加密IV不一致。3. 密文被篡改。4. 算法/模式/填充字符串与加密时不匹配。1.仔细核对密钥确保加密解密使用的是同一个Base64字符串。2.仔细核对IV特别是使用固定IV或从组合字符串中拆分时。3. 确保数据完整性。如果是网络传输考虑增加签名验证。4. 检查Cipher.getInstance()传入的字符串是否完全一致。java.lang.IllegalArgumentException: IV must be 16 bytes long传递给IvParameterSpec的字节数组长度不是16。检查生成或解码IV的代码。如果是固定IV确保是16字节。如果是组合字符串拆分检查分隔符和拆分逻辑。解密后得到乱码1. 密钥/IV错误但未抛BadPaddingException可能数据恰好能解。2. 明文字符编码与解密后字节数组转字符串时使用的编码不一致。1. 用已知的、简单的明文如“test”进行加密解密测试排除业务数据干扰。2.强制使用UTF-8编码new String(decryptedBytes, StandardCharsets.UTF_8)。6.2 调试技巧编写单元测试与交叉验证一个健壮的加解密模块必须有完善的单元测试。基础功能测试测试加密后再解密是否能还原原始文本。随机性测试用同一密钥加密同一明文多次使用随机IV结果应不同。固定向量测试使用固定的密钥和IV确保每次加密结果相同。这对于回归测试和与第三方对接验证至关重要。交叉验证这是最重要的技巧。如果你需要和其他语言如Python、C#的系统对接不要等到联调时才发现问题。自己用Python如gmssl库或在线工具注意使用可信的、离线的测试工具写一个简单的加密脚本用相同的密钥、IV、模式和填充去加密一个字符串看结果是否和你的Java工具类输出一致。这能快速定位是编码问题、IV处理问题还是算法本身的问题。import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class Sm4UtilTest { private static final String TEST_KEY_BASE64 你的16字节密钥Base64; private static final String TEST_IV_BASE64 你的16字节IV Base64; // 用于固定IV测试 private static final String TEST_PLAIN_TEXT Hello, SM4! 测试123; Test void testEncryptDecryptWithRandomIv() throws Exception { String combinedResult Sm4Util.encryptTextWithIv(TEST_PLAIN_TEXT, TEST_KEY_BASE64); String decrypted Sm4Util.decryptTextWithIv(combinedResult, TEST_KEY_BASE64); assertEquals(TEST_PLAIN_TEXT, decrypted); } Test void testEncryptDecryptWithFixedIv() throws Exception { // 测试固定IV结果应可重复 String cipher1 Sm4Util.encryptText(TEST_PLAIN_TEXT, TEST_KEY_BASE64, TEST_IV_BASE64); String cipher2 Sm4Util.encryptText(TEST_PLAIN_TEXT, TEST_KEY_BASE64, TEST_IV_BASE64); assertEquals(cipher1, cipher2); // 固定IV密文应相同 String decrypted Sm4Util.decryptText(cipher1, TEST_KEY_BASE64, TEST_IV_BASE64); assertEquals(TEST_PLAIN_TEXT, decrypted); } Test void testDifferentIvProduceDifferentCipher() throws Exception { // 同一密钥不同随机IV密文应不同 String result1 Sm4Util.encryptTextWithIv(TEST_PLAIN_TEXT, TEST_KEY_BASE64); String result2 Sm4Util.encryptTextWithIv(TEST_PLAIN_TEXT, TEST_KEY_BASE64); // 拆分出密文部分进行比较 String cipher1 result1.split(:)[1]; String cipher2 result2.split(:)[1]; assertNotEquals(cipher1, cipher2); } }6.3 密钥管理的最佳实践雏形工具类解决了“怎么用”而“密钥放哪”是更关键的安全问题。在生产环境中绝对避免硬编码。环境变量将Base64编码的密钥放在应用服务器的环境变量中。这是最简单的方式但权限控制要做好。配置中心放入Apollo、Nacos等配置中心并利用其加密功能对密钥进行二次加密。密钥管理服务KMS在云环境或大型系统中使用专业的KMS如阿里云KMS、腾讯云KMS、HashiCorp Vault来生成、存储和轮换密钥。应用在启动时从KMS动态获取密钥内存中使用不落盘。硬件安全模块HSM最高安全等级的场景使用HSM进行加解密运算密钥永远不出硬件设备。对于大多数应用从环境变量或配置中心读取已经是一个良好的起点。记住一个原则密钥的秘密性与其被多少人、多少地方知晓成反比。知道的地方越少越安全。走到这里你已经拥有了一个从原理到实现再到生产级考量的完整SM4加解密能力。它不再是一个黑盒API调用而是一个你知其然也知其所以然并且知道如何驯服它的工具。技术总是在迭代国密算法的推广也在不断深入掌握其核心原理和实现方法能让你在未来的项目中更加从容。下次再遇到加密需求时除了AES你的工具箱里已经多了一个可靠且合规的选择。