国密SM4前后端互通实战:JavaScript与Java加解密全流程详解 1. 项目概述为什么SM4互通是个“技术活”最近在做一个涉及金融数据安全传输的项目前端是Vue后端是Spring Boot甲方爸爸明确要求核心敏感字段必须使用国密SM4算法进行加解密。这要求听起来挺合理对吧国密算法自主可控安全合规。但真干起来才发现从JavaScript到Java的SM4互通简直是一个接一个的坑。你以为两边都调个库传个密文就完事了Too young, too simple.最典型的场景就是前端用JavaScript加密一串用户身份证号洋洋洒洒传到后端Java这边一解密要么直接报错要么解出来一堆乱码。你盯着控制台那串看似完美的Hex或Base64字符串百思不得其解——明明算法都是SM4怎么就不通了呢这背后涉及到的远不止一个算法调用那么简单。它关乎加密模式是ECB还是CBC、填充方式PKCS#5还是PKCS#7、密钥与IV的处理是字符串还是字节数组编码是啥、以及数据格式的转换Hex, Base64, 还是直接传字节。任何一个环节对不上整个流程就崩了。这个项目折腾了我差不多一周把bcprov-jdk18on、sm-crypto这些库翻了个底朝天才把这条互通之路跑通。今天我就把其中最关键的技术点、最容易踩的坑以及最终的解决方案掰开揉碎了跟大家聊聊。无论你是前端同学需要对接国密后端还是后端同学要提供国密接口给前端这篇“避坑指南”都能让你少走很多弯路。我们的目标很简单让数据安全、正确地在浏览器和服务器之间跑起来。2. 核心概念与互通难点拆解在动手写代码之前我们必须把SM4互通的基本概念和那些“魔鬼细节”搞清楚。很多人一上来就找代码结果就是不断试错浪费大量时间。2.1 国密SM4算法简介SM4是一种分组密码算法分组长度和密钥长度均为128位即16字节。这意味着它一次处理16个字节的数据。对于不足16字节的数据就需要进行填充Padding对于超过16字节的数据就需要进行分组迭代这就引出了不同的工作模式。算法本身是标准的但如何运用这个算法就产生了不同的“配方”这也是互通的第一个拦路虎。2.2 互通的核心四要素要让JavaScript和Java端的SM4加解密结果一致以下四个要素必须完全匹配缺一不可密钥Key必须是128位16字节。如果给你的密钥是字符串比如一个密码那么你需要一个双方一致的转换规则将其变成16字节的数组。常见的做法是使用MD5、SHA-256等哈希函数对字符串密钥进行摘要然后取前16字节或者直接对字符串进行UTF-8编码后截断/补位。关键在于两端用于加密和解密的字节数组必须一模一样。初始化向量IV在使用CBC、CFB等模式时必需。IV也是一个16字节的数组用于增加加密的随机性防止相同的明文生成相同的密文。IV不需要保密但必须随机生成并且在一次加密和解密过程中保持一致。通常前端随机生成IV将其和密文一起传给后端或者双方约定一个固定的IV安全性较低不推荐用于高敏感数据。加密模式Mode最常见的是ECB和CBC。ECB电子密码本最简单每个分组独立加密。缺点非常明显相同的明文分组会得到相同的密文分组容易受到攻击不推荐用于加密有规律的数据。但它的好处是无需IV实现简单。CBC密码分组链接当前一个分组的密文参与下一个分组的加密运算增强了安全性。这是目前最推荐、也是最常用的模式。使用CBC必须配合IV。填充方式Padding因为SM4是分组加密必须处理数据长度不是16字节整数倍的情况。PKCS#5/PKCS#7这是最常用的填充方式。本质上PKCS#5是PKCS#7的子集仅用于8字节分组如DES。对于16字节分组的SM4我们说的PKCS#5填充实际就是指PKCS#7。它的规则是缺少N个字节就填充N个值为N的字节。例如明文最后缺少3个字节则填充0x03 0x03 0x03。NoPadding不填充。这就要求你加密的数据长度必须是16字节的整数倍否则会出错。一般用于自己已经处理好填充的场景。关键避坑点1默认配置陷阱不同的加密库其默认的Mode和Padding可能不同比如某个Java库默认是ECB/PKCS5Padding而某个JavaScript库默认可能是CBC/PKCS7Padding。如果你不显式指定那么两端默认不一致必然导致失败。最佳实践是无论在JS端还是Java端都显式、明确地指定Mode和Padding。2.3 数据格式的约定加解密操作的对象是字节数组byte[]或Uint8Array。但我们在网络传输或存储时通常使用可打印的字符串格式。这就需要编码和解码。Hex十六进制将每个字节转换为两个十六进制字符。例如字节0xAB表示为字符串AB。长度会扩大一倍但可读性好。Base64将3个字节编码为4个可打印字符。长度增加约33%是网络传输中最常用的格式因为它比Hex更紧凑并且可以安全地放在URL、JSON中。互通时必须约定好传输的密文和IV是什么格式。常见做法是前端将密文和IV都转换为Base64字符串通过JSON传给后端后端收到后先进行Base64解码得到字节数组再进行解密。3. 前端JavaScriptWeb实现详解前端我们选用一个比较成熟且维护良好的国密算法库sm-crypto。它支持SM2、SM3、SM4且纯JavaScript实现不依赖任何本地模块非常适合浏览器环境。3.1 环境准备与库安装首先在你的前端项目如Vue、React或纯HTML项目中安装sm-crypto。npm install sm-crypto --save # 或 yarn add sm-crypto安装后在需要的组件或模块中引入SM4模块import { sm4 } from sm-crypto;3.2 核心加密函数实现假设我们与后端约定使用CBC模式、PKCS7填充。密钥是一个16字节的字符串例如1234567890abcdef。注意如果密钥字符串不是16字节你需要先将其转换为16字节方法后面会讲。这里我们实现一个完整的加密函数包含IV的生成和处理。/** * 使用SM4 CBC模式加密文本 * param {string} plaintext - 待加密的明文 * param {string} key - 密钥字符串需为16字节长度 * returns {object} 返回包含密文和IV的对象均为Base64格式 */ function encryptSM4CBC(plaintext, key) { // 1. 检查密钥长度UTF-8编码下的字节长度 const keyBytes new TextEncoder().encode(key); if (keyBytes.length ! 16) { throw new Error(密钥必须为16字节16个英文字符或8个中文字符); } // 2. 生成16字节的随机IV (Initialization Vector) const ivArray new Uint8Array(16); crypto.getRandomValues(ivArray); // 使用Web Crypto API生成密码学安全的随机数 // 3. 执行加密 // sm4.encrypt() 参数说明 (明文, 密钥, 配置对象) // 配置对象: { mode: cbc, iv: iv数组 } 默认填充是PKCS7 const encryptData sm4.encrypt(plaintext, key, { mode: cbc, iv: ivArray, // 传入Uint8Array格式的IV }); // 4. 数据转换与返回 // sm-crypto的encrypt方法默认返回16进制(Hex)字符串。 // 但为了传输方便我们将其和IV都转为Base64。 const cipherTextBase64 hexToBase64(encryptData); const ivBase64 arrayBufferToBase64(ivArray.buffer); return { cipherText: cipherTextBase64, iv: ivBase64, }; } // 辅助函数16进制字符串转Base64 function hexToBase64(hexString) { // 将16进制字符串转换为字节数组 const byteArray new Uint8Array(hexString.match(/.{1,2}/g).map(byte parseInt(byte, 16))); // 将字节数组转换为Base64 return btoa(String.fromCharCode.apply(null, byteArray)); } // 辅助函数ArrayBuffer转Base64 function arrayBufferToBase64(buffer) { const bytes new Uint8Array(buffer); const binary bytes.reduce((acc, byte) acc String.fromCharCode(byte), ); return btoa(binary); }3.3 核心解密函数实现解密是加密的逆过程。我们需要从后端接收或从存储中取得Base64格式的密文和IV。/** * 使用SM4 CBC模式解密文本 * param {string} cipherTextBase64 - Base64格式的密文 * param {string} key - 密钥字符串需为16字节长度 * param {string} ivBase64 - Base64格式的IV * returns {string} 解密后的明文 */ function decryptSM4CBC(cipherTextBase64, key, ivBase64) { // 1. 将Base64格式的密文和IV转换为16进制字符串sm-crypto解密需要Hex输入 const cipherTextHex base64ToHex(cipherTextBase64); const ivArray base64ToUint8Array(ivBase64); // 2. 执行解密 // sm4.decrypt() 参数说明 (密文Hex, 密钥, 配置对象) const decryptData sm4.decrypt(cipherTextHex, key, { mode: cbc, iv: ivArray, }); return decryptData; // 解密结果已是字符串 } // 辅助函数Base64转16进制字符串 function base64ToHex(base64String) { const raw atob(base64String); let result ; for (let i 0; i raw.length; i) { const hex raw.charCodeAt(i).toString(16); result (hex.length 2 ? hex : 0 hex); } return result; } // 辅助函数Base64转Uint8Array function base64ToUint8Array(base64String) { const binaryString atob(base64String); const bytes new Uint8Array(binaryString.length); for (let i 0; i binaryString.length; i) { bytes[i] binaryString.charCodeAt(i); } return bytes; }3.4 密钥处理与安全注意事项很多时候我们获得的密钥可能是一个任意长度的字符串比如用户输入的密码而不是标准的16字节。处理方法使用哈希函数如SM3生成固定长度的密钥。sm-crypto也提供了SM3哈希功能。import { sm3 } from sm-crypto; /** * 从任意字符串生成16字节的SM4密钥 * param {string} password - 任意长度的密码字符串 * returns {string} 16字节的Hex字符串形式的密钥 */ function generateSM4KeyFromPassword(password) { // 1. 使用SM3对密码进行哈希得到32字节64位Hex的摘要 const hashHex sm3(password); // 输出是64字符的Hex字符串 // 2. 取前32个字符即前16字节作为SM4密钥 const sm4KeyHex hashHex.substring(0, 32); // 3. 如果你想直接得到字符串形式的密钥可以将其Hex转回ASCII但要求这16字节是可打印字符 // 更通用的做法是直接使用这个Hex字符串作为密钥但注意sm-crypto的encrypt/decrypt方法要求密钥是字符串。 // 实际上sm-crypto的encrypt方法内部会处理Hex密钥。 // 所以我们可以直接返回这个Hex字符串并在加密时使用。 return sm4KeyHex; } // 使用示例 const userPassword MySecretPassword123; const derivedKeyHex generateSM4KeyFromPassword(userPassword); console.log(Derived Key (Hex):, derivedKeyHex); // 长度为32的字符串 // 加密时直接将这个Hex字符串作为key参数传入 const encrypted sm4.encrypt(hello world, derivedKeyHex, { mode: cbc, iv: someIV });关键避坑点2密钥一致性前端使用sm3(password).substring(0, 32)生成的Hex密钥后端必须用完全相同的算法和步骤生成相同的字节数组。如果后端用password.getBytes(UTF-8)然后取前16字节那结果肯定对不上。因此前后端必须严格约定密钥派生算法。4. 后端Java实现详解后端我们使用Bouncy Castle这个强大的密码学提供者它提供了对国密算法的完整支持。4.1 依赖引入与环境配置在Maven项目的pom.xml中添加依赖。推荐使用bcprov-jdk18on它支持到JDK 18。dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78/version !-- 请使用最新稳定版本 -- /dependency在应用启动时或者在使用加密解密功能之前需要将Bouncy Castle注册为安全提供者。import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class SecurityConfig { static { // 注册Bouncy Castle Provider if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } }4.2 核心工具类构建我们创建一个Sm4Util工具类封装加密和解密方法。这里同样采用CBC模式、PKCS7填充在BC中通常指定为PKCS5Padding对于16字节分组它实际执行PKCS7。import lombok.extern.slf4j.Slf4j; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.Security; Slf4j public class Sm4Util { private static final String ALGORITHM_NAME SM4; private static final String TRANSFORMATION_CBC SM4/CBC/PKCS5Padding; // 使用PKCS5PaddingBC会按PKCS7处理 private static final String TRANSFORMATION_ECB SM4/ECB/PKCS5Padding; private static final int KEY_LENGTH 16; // 128 bits static { // 确保提供者已注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); } } /** * SM4 CBC模式加密 * param plaintext 明文 * param keyBytes 16字节的密钥 * param ivBytes 16字节的初始化向量 * return Base64编码的密文 */ public static String encryptCbc(byte[] plaintext, byte[] keyBytes, byte[] ivBytes) throws Exception { validateKeyAndIv(keyBytes, ivBytes); Cipher cipher Cipher.getInstance(TRANSFORMATION_CBC, BouncyCastleProvider.PROVIDER_NAME); SecretKeySpec secretKeySpec new SecretKeySpec(keyBytes, ALGORITHM_NAME); IvParameterSpec ivParameterSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); byte[] encryptedBytes cipher.doFinal(plaintext); return Base64.toBase64String(encryptedBytes); } /** * SM4 CBC模式解密 * param cipherTextBase64 Base64编码的密文 * param keyBytes 16字节的密钥 * param ivBytes 16字节的初始化向量 * return 解密后的明文字节数组 */ public static byte[] decryptCbc(String cipherTextBase64, byte[] keyBytes, byte[] ivBytes) throws Exception { validateKeyAndIv(keyBytes, ivBytes); byte[] cipherBytes Base64.decode(cipherTextBase64); Cipher cipher Cipher.getInstance(TRANSFORMATION_CBC, BouncyCastleProvider.PROVIDER_NAME); SecretKeySpec secretKeySpec new SecretKeySpec(keyBytes, ALGORITHM_NAME); IvParameterSpec ivParameterSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); return cipher.doFinal(cipherBytes); } /** * 验证密钥和IV长度 */ private static void validateKeyAndIv(byte[] keyBytes, byte[] ivBytes) { if (keyBytes.length ! KEY_LENGTH) { throw new IllegalArgumentException(密钥长度必须为16字节); } if (ivBytes ! null ivBytes.length ! KEY_LENGTH) { throw new IllegalArgumentException(IV长度必须为16字节); } } /** * 从密码派生SM4密钥使用SM3哈希取前16字节 * 注意此方法需要bcprov-ext-jdk18on依赖以使用SM3或使用其他SM3实现。 * 这里为简化先使用SHA-256示例。确保与前端的派生算法一致 */ public static byte[] generateSm4KeyFromPassword(String password) throws Exception { // 重要这里必须使用与前端的JavaScript端完全相同的算法 // 前端使用 SM3(password).substring(0, 32) (Hex) // 后端也需要用SM3计算哈希。 // 假设我们有一个SM3的工具类 Sm3Util.digest(password) // byte[] hash Sm3Util.digest(password.getBytes(StandardCharsets.UTF_8)); // return Arrays.copyOf(hash, 16); // 取前16字节 // 临时示例使用SHA-256 (仅用于演示务必与前端对齐) java.security.MessageDigest md java.security.MessageDigest.getInstance(SHA-256); byte[] hash md.digest(password.getBytes(StandardCharsets.UTF_8)); return java.util.Arrays.copyOf(hash, 16); } }4.3 在Spring Boot控制器中的应用现在我们创建一个REST接口来接收前端加密的数据并解密。import org.springframework.web.bind.annotation.*; import java.nio.charset.StandardCharsets; RestController RequestMapping(/api/sm4) public class Sm4Controller { // 假设这是一个双方预先约定好的固定密钥示例生产环境应从安全配置读取 private static final String SECRET_KEY 1234567890abcdef; // 16字节字符串 PostMapping(/decrypt) public ApiResponse decryptData(RequestBody EncryptRequest request) { try { // 1. 将固定密钥字符串转换为字节数组 byte[] keyBytes SECRET_KEY.getBytes(StandardCharsets.UTF_8); // 2. 前端传来的IV是Base64格式需要解码 byte[] ivBytes java.util.Base64.getDecoder().decode(request.getIv()); // 3. 执行解密 byte[] decryptedBytes Sm4Util.decryptCbc(request.getCipherText(), keyBytes, ivBytes); String plaintext new String(decryptedBytes, StandardCharsets.UTF_8); // 4. 返回解密结果 return ApiResponse.success(plaintext); } catch (Exception e) { log.error(SM4解密失败, e); return ApiResponse.error(解密失败: e.getMessage()); } } // 请求体 Data // 使用Lombok public static class EncryptRequest { private String cipherText; // Base64密文 private String iv; // Base64 IV } // 响应体 Data public static class ApiResponse { private int code; private String message; private Object data; public static ApiResponse success(Object data) { ApiResponse resp new ApiResponse(); resp.code 200; resp.message success; resp.data data; return resp; } public static ApiResponse error(String msg) { ApiResponse resp new ApiResponse(); resp.code 500; resp.message msg; return resp; } } }5. 前后端联调与互通实战理论说完代码写完最激动人心也最容易崩溃的联调环节来了。这里我模拟一个完整的流程并附上每一步的检查点。5.1 完整流程演练场景前端需要加密用户手机号13800138000并发送给后端。第1步前端加密确定密钥双方约定密钥为字符串my-secret-key-16。注意这个字符串的UTF-8字节长度正好是16。执行加密const plaintext 13800138000; const key my-secret-key-16; const encryptedResult encryptSM4CBC(plaintext, key); console.log(加密结果:, encryptedResult); // 输出可能类似: { cipherText: L4A8...xx, iv: kR8q...Yf0 }将encryptedResult.cipherText和encryptedResult.iv作为JSON参数发起请求。第2步网络传输{ cipherText: L4A8zT...Base64字符串, iv: kR8qFg...Base64字符串 }第3步后端解密后端接收到JSON提取cipherText和iv。使用相同的密钥字符串my-secret-key-16转换为UTF-8字节数组。对iv进行Base64解码得到IV字节数组。调用Sm4Util.decryptCbc(cipherText, keyBytes, ivBytes)。将解密后的字节数组用UTF-8解码成字符串得到13800138000。5.2 联调检查清单避坑宝典当你的加解密不通时请按照以下清单逐一排查99%的问题都能找到检查项前端JavaScript后端Java排查命令/方法1. 密钥一致性密钥字符串的字节表示是否16位是否经过了哈希派生密钥字节数组是否与前端的字节表示完全一致派生算法是否相同前端console.log(new TextEncoder().encode(key).length)后端System.out.println(keyBytes.length);并打印Hex对比。2. 加密模式sm4.encrypt是否指定{ mode: cbc }Cipher.getInstance是否使用SM4/CBC/...确认代码中显式指定了CBC。3. 填充方式sm-crypto默认PKCS7是否更改Cipher.getInstance是否使用.../PKCS5Padding保持两端均为PKCS7/PKCS5Padding。4. IV处理IV是否随机生成并参与加密IV是否随密文一起传输解密时使用的IV是否与加密时的IV解码后完全相同对比传输的Base64 IV字符串解码后比较字节数组。5. 数据格式传给后端的密文和IV是否是Base64字符串收到后是否先进行Base64解码再进行解密操作前端typeof cipherText string且符合Base64特征。后端使用Base64.getDecoder().decode()。6. 字符编码明文转字节、密钥转字节是否使用UTF-8解密后字节转字符串是否使用UTF-8前后端统一使用UTF-8。Java中明确指定StandardCharsets.UTF_8。7. 库与提供商使用的是sm-crypto库。已添加bcprov依赖并注册BouncyCastleProvider。后端检查Security.getProviders()是否包含BC。关键避坑点3字节级的对比调试当出现问题时不要只看字符串。将前后端在关键步骤如密钥、IV、加密前的明文、解密后的字节的数据都以十六进制Hex的形式打印出来进行对比。一个字节的差异都会导致失败。例如在Java端System.out.println(Hex.toHexString(keyBytes));在JS端console.log(arrayBufferToHex(keyBuffer))。5.3 一个实用的联调试错示例假设后端解密报错javax.crypto.BadPaddingException: pad block corrupted。这个错误通常意味着密钥错了。IV错了。密文在传输或解码过程中被篡改或损坏。加密模式或填充不匹配。调试步骤隔离测试写一个简单的Java单元测试用固定的密钥、IV和密文从前端日志中复制解密看是否成功。如果单元测试成功问题可能出在网络传输或接口层。日志输出在前后端的关键节点打印Hex值。前端打印加密前的明文Hex、密钥Hex、生成的IV Hex、加密后的密文Hex。后端打印接收到的Base64密文和IV解码后的Hex以及从配置中读取的密钥Hex。逐项对比对比密钥Hex是否完全一致。对比IV Hex是否完全一致。手动将前端的密文Hex进行Base64编码看是否与传输的Base64字符串一致验证传输过程。通过以上对比几乎一定能定位到是哪个环节的数据出现了偏差。6. 进阶话题与性能优化当基本功能跑通后我们可能会考虑更多实际生产中的问题。6.1 使用GCM模式实现认证加密CBC模式能保证机密性但不能保证密文的完整性即无法防止密文被篡改。GCMGalois/Counter Mode模式同时提供了机密性和完整性认证是更推荐的选择。sm-crypto和Bouncy Castle都支持SM4-GCM。前端JS (sm-crypto) 注意事项sm-crypto的GCM模式调用与CBC类似但需要指定额外的additionalData可选和tagLength通常为128位。const encrypted sm4.encrypt(hello world, key, { mode: gcm, iv: ivArray, additionalData: some-auth-data, // 可选 tagLength: 128 // 位 });GCM加密输出的密文已经包含了认证标签Tag。解密时需要相同的配置。后端Java (Bouncy Castle) 实现 需要使用GCMParameterSpec。public static String encryptGcm(byte[] plaintext, byte[] keyBytes, byte[] ivBytes) throws Exception { Cipher cipher Cipher.getInstance(SM4/GCM/NoPadding, BouncyCastleProvider.PROVIDER_NAME); SecretKeySpec keySpec new SecretKeySpec(keyBytes, SM4); // GCM通常使用12字节96位的IV标签长度128位 GCMParameterSpec gcmSpec new GCMParameterSpec(128, ivBytes); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); // 可以添加附加认证数据(AAD) // cipher.updateAAD(additionalData.getBytes(StandardCharsets.UTF_8)); byte[] encrypted cipher.doFinal(plaintext); return Base64.toBase64String(encrypted); }GCM解密时密文包含了标签初始化方式相同。关键避坑点4GCM的IV长度GCM模式推荐使用12字节96位的IV而不是CBC的16字节。前后端需要对此进行约定。如果使用16字节IVBC可能会自动处理但最好遵循标准。6.2 性能考量与最佳实践密钥管理绝对不要将硬编码的密钥放在前端代码中。前端密钥应通过安全通道如HTTPS从服务端动态获取或使用非对称加密如SM2来协商对称密钥。生产环境中后端密钥也应存放在安全的配置中心或硬件安全模块HSM中。IV的生成与传递每次加密都应使用随机生成的IV。IV可以公开传递但必须保证唯一性和随机性使用密码学安全的随机数生成器。可以将IV预置在密文前一起传输例如IV CipherText也可以作为单独字段传输。错误处理加解密操作必须进行完整的异常捕获。不要将底层的加密异常如BadPaddingException直接抛给用户应转换为统一的业务异常信息。数据长度对称加密适合加密数据块。对于大文件应使用流式加密或先分段加密。注意使用CBC等模式时加密后的数据长度会因为填充而增加。依赖版本保持sm-crypto和bcprov库的版本稳定并关注更新日志。不同版本间可能会有细微的兼容性差异。7. 常见问题排查实录这里记录了几个我在实际开发中遇到的典型问题及其解决方案。问题1前端加密成功后端解密报InvalidKeyException: Illegal key size原因早期JDK有默认的加密强度限制JCE策略限制。SM4的128位密钥可能受此限制。解决对于JDK 8u151及以上版本默认已解除限制。如果使用旧版本需要手动下载并替换local_policy.jar和US_export_policy.jar两个JAR包到$JAVA_HOME/jre/lib/security/目录下。更简单的方法是升级JDK。问题2解密后得到乱码但长度似乎正确原因字符编码不一致。前端使用TextEncoder通常是UTF-8将字符串转为字节后端解密后可能使用了错误的字符集如GBK来还原字符串。解决确保前后端在所有字符串与字节数组转换的地方都明确指定UTF-8。Java端使用new String(decryptedBytes, StandardCharsets.UTF_8)。问题3在Android或特定Java环境中无法找到SM4算法原因Bouncy Castle Provider未正确注册或者注册的优先级不够高。解决确认依赖已引入。在调用加解密代码前确保执行了Security.addProvider(new BouncyCastleProvider())。可以在Cipher.getInstance时强制指定提供者Cipher.getInstance(SM4/CBC/PKCS5Padding, BC)。但更推荐在程序启动时全局注册。问题4使用Hex格式密钥时加解密失败原因sm-crypto的encrypt方法接受Hex字符串作为密钥但Java的SecretKeySpec需要字节数组。如果你将Hex字符串直接getBytes()就错了。解决需要将Hex字符串解码为字节数组。// 错误的做法 // byte[] keyBytes hexKeyString.getBytes(StandardCharsets.UTF_8); // 正确的做法 import org.bouncycastle.util.encoders.Hex; byte[] keyBytes Hex.decode(hexKeyString); // 使用BC库的Hex解码折腾完这一整套最大的体会就是密码学互通细节决定成败。它不像调用一个普通的API参数对了就能返回结果。它要求前后端工程师对算法、编码、数据格式有着完全一致的理解和实现。最好的办法就是在一开始就定好一份详细的“加密通信协议”把算法、模式、填充、密钥派生方法、IV生成与传递方式、数据编码格式全部白纸黑字写清楚双方都严格按照协议实现。这样能节省大量无效的联调时间。希望这篇长文能成为你国密SM4互通之路上的一个实用路书帮你把坑都填平。