1. 认识SM2国密算法第一次接触SM2算法是在一个政务系统项目中客户明确要求必须使用国密算法。当时我对这个名词还很陌生后来才知道这是我国自主研发的一套密码算法标准。SM2作为其中的非对称加密算法基于椭圆曲线密码学ECC相比传统的RSA算法有着明显的优势。简单来说SM2就像是一把特殊的数字钥匙。它采用256位的密钥长度却能提供相当于RSA 2048位的安全强度。在实际测试中我发现SM2的签名速度比RSA快得多特别是在移动设备上这个优势更加明显。这让我想起第一次用SM2替换RSA时的场景原本需要2秒的签名操作现在只需要200毫秒性能提升了近10倍。2. 环境搭建与基础配置2.1 引入BouncyCastle依赖要在Java中使用SM2BouncyCastle是必不可少的工具包。我推荐使用最新稳定版目前是1.70版本。在Maven项目中这样配置dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15to18/artifactId version1.70/version /dependency这里有个小坑要注意不同JDK版本要选择对应的BouncyCastle包。比如JDK8可以用bcprov-jdk15on而JDK11及以上建议用bcprov-jdk15to18。我曾经因为用错版本导致各种奇怪的NoSuchMethodError排查了半天才发现是版本不匹配的问题。2.2 初始化安全提供者在使用前必须注册BouncyCastle提供者。我习惯在静态代码块中完成这个操作static { if (Security.getProvider(BC) null) { Security.addProvider(new BouncyCastleProvider()); } }这里有个经验分享在Web应用中我遇到过多个线程同时注册提供者导致的问题。所以最好在应用启动时就完成注册避免并发问题。3. SM2核心功能实现3.1 密钥对生成生成SM2密钥对是第一步也是后续所有操作的基础。这是我优化后的密钥生成代码public static KeyPair generateKeyPair() throws Exception { ECGenParameterSpec sm2Spec new ECGenParameterSpec(sm2p256v1); KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); kpg.initialize(sm2Spec, new SecureRandom()); return kpg.generateKeyPair(); }在实际项目中我发现密钥生成是个相对耗时的操作。对于需要频繁生成密钥的场景可以预生成一批密钥存入缓存。我曾经测试过生成1000个密钥对大约需要3秒而直接从内存读取几乎不耗时。3.2 数据加密解密加密实现需要注意几个关键点。首先是处理明文的长度限制SM2单次加密的数据量有限。这是我的分段加密方案public static byte[] encrypt(PublicKey publicKey, byte[] data) throws Exception { Cipher cipher Cipher.getInstance(SM2, BC); cipher.init(Cipher.ENCRYPT_MODE, publicKey); int blockSize 64; // 根据实际情况调整 ByteArrayOutputStream out new ByteArrayOutputStream(); for (int i 0; i data.length; i blockSize) { int end Math.min(data.length, i blockSize); byte[] block cipher.doFinal(data, i, end - i); out.write(block); } return out.toByteArray(); }解密时要注意密文的格式。SM2加密后的数据包含三部分C1曲线点、C2密文、C3摘要。我曾经因为顺序搞错导致解密失败后来发现BouncyCastle其实已经帮我们处理好了这些细节。4. 签名与验签实战4.1 生成数字签名SM2的签名算法和ECDSA有所不同它采用了特定的签名方案。这是我封装好的签名方法public static byte[] sign(PrivateKey privateKey, byte[] data) throws Exception { Signature signature Signature.getInstance(SM3withSM2, BC); signature.initSign(privateKey); signature.update(data); return signature.sign(); }这里使用了SM3作为摘要算法。在实际测试中我发现SM3withSM2的组合比SHA256withECDSA快约30%。有个性能优化技巧对于大文件签名可以先计算SM3摘要再对摘要进行签名这样能显著减少内存使用。4.2 验证签名验签过程相对简单但要注意异常处理。这是我项目中使用的验签代码public static boolean verify(PublicKey publicKey, byte[] data, byte[] signature) { try { Signature verifier Signature.getInstance(SM3withSM2, BC); verifier.initVerify(publicKey); verifier.update(data); return verifier.verify(signature); } catch (Exception e) { log.error(验签失败, e); return false; } }在金融项目中我们遇到过验签性能瓶颈。通过将验签操作放入线程池并行处理吞吐量提升了5倍。关键是要注意线程安全和资源控制避免创建过多线程。5. 性能调优实战5.1 对象复用优化在高并发场景下频繁创建加密对象会导致大量GC。我的解决方案是使用ThreadLocal缓存对象private static final ThreadLocalCipher cipherHolder ThreadLocal.withInitial(() - { try { return Cipher.getInstance(SM2, BC); } catch (Exception e) { throw new RuntimeException(e); } });这样每个线程都有自己的Cipher实例既避免了线程安全问题又减少了对象创建开销。实测下来QPS从1000提升到了3500左右。5.2 批量操作优化对于批量加密场景我开发了并行处理工具类public Listbyte[] batchEncrypt(Listbyte[] dataList, PublicKey publicKey) { return dataList.parallelStream() .map(data - { try { return encrypt(publicKey, data); } catch (Exception e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); }使用parallelStream可以充分利用多核CPU。在处理10000条数据时串行加密需要12秒而并行处理仅需3秒。注意要根据服务器核心数调整并行度避免过度竞争。5.3 内存管理技巧大文件加密时容易OOM我采用了流式处理方案public void encryptFile(PublicKey publicKey, Path input, Path output) throws Exception { try (InputStream in Files.newInputStream(input); OutputStream out Files.newOutputStream(output)) { Cipher cipher Cipher.getInstance(SM2, BC); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead in.read(buffer)) ! -1) { byte[] encrypted cipher.update(buffer, 0, bytesRead); if (encrypted ! null) { out.write(encrypted); } } byte[] encrypted cipher.doFinal(); out.write(encrypted); } }这种方式无论文件多大内存占用都保持稳定。曾经处理过一个10GB的文件内存使用始终保持在20MB左右。6. 常见问题排查6.1 密钥格式问题经常遇到的问题是密钥格式不兼容。比如从其他系统获取的密钥可能需要转换public static PublicKey convertPublicKey(byte[] keyBytes) throws Exception { X509EncodedKeySpec keySpec new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(EC, BC); return keyFactory.generatePublic(keySpec); }特别是和硬件设备交互时各家的密钥格式可能不同。我整理了一个密钥转换工具类支持PEM、DER等多种格式的相互转换。6.2 性能突然下降有次上线后加密性能突然下降后来发现是因为SecureRandom的种子问题。解决方案是使用更好的随机源SecureRandom secureRandom SecureRandom.getInstanceStrong();在Linux服务器上还可以使用/dev/urandom作为随机源。这个问题让我印象深刻因为性能差异可以达到10倍之多。6.3 跨平台兼容性Android和Java的标准实现有些差异。针对Android的特别处理public static Signature getSignatureInstance() throws Exception { try { return Signature.getInstance(SM3withSM2, BC); } catch (Exception e) { // Android备用方案 return Signature.getInstance(ECDSA, BC); } }在混合开发环境中最好进行充分的跨平台测试。我们曾经因为这个问题导致Android用户无法验证iOS生成的签名。