第2章 Java加密基础:JCA/JCE架构

做Java加密,绕不开JCA/JCE。说白了,它就是JDK自带的加密框架。我记得刚开始接触国密SDK时,还想过自己从头写SM2/SM3算法——幸好没干那种傻事。后来我发现,用好JCA/JCE这套抽象层,接入国密就跟换一个Provider差不多。

这一章我们就把核心的几个类过一遍。你不用死记硬背,知道它们在哪、怎么用就行。后面章节要封装国密SDK时,会反复用到这些东西。

2.1 JCA/JCE 到底是什么

JCA(Java Cryptography Architecture)是加密架构,JCE(Java Cryptography Extension)是扩展包。早期的JDK把加密限制在128位以内,JCE就是用来突破那个限制的。不过现在JDK 8以上已经默认包含强加密策略,不用再单独下载JCE包了。

这套架构的核心设计理念:Provider 分离。具体实现和接口完全解耦。你写代码时调的都是标准API(比如 Cipher.getInstance("AES")),实际干活的是后台的Provider。这种方式的好处是:想换算法?换个Provider名就行,代码几乎不用改。

2.2 核心类一览

JCA/JCE 提供了一组核心引擎类。我把它们整理成了一张表,你对照着看会更清楚。

引擎类功能常用算法举例
java.security.Provider加密服务提供者,管理算法实现SunJCE、BC、GMJCE
javax.crypto.Cipher对称/非对称加密、解密AES、SM4、RSA、SM2
java.security.Signature数字签名生成与验证SHA256withRSA、SM3withSM2
java.security.MessageDigest消息摘要(哈希)MD5、SHA-256、SM3
java.security.KeyGenerator对称密钥生成器AES、SM4
java.security.KeyPairGenerator非对称密钥对生成器RSA、SM2
java.security.KeyStore密钥库管理JKS、PKCS12
个人小经验:我习惯把这张表打印出来贴工位上。每次做新项目,先确认需要哪些引擎类,然后找对应的国密Provider实现即可。省得一个个翻文档。

2.3 Provider —— 加密服务提供者

Provider 是JCA的入口。它维护一个算法名到实际实现的映射。你调用 Cipher.getInstance("SM4") 时,JCA会遍历注册过的Provider列表,找到能处理 "SM4" 的那个Provider,然后把你请求委派给它。

注册Provider

最常用的两种方式:

  • 静态注册:修改 $JAVA_HOME/jre/lib/security/java.security 配置文件,加入 security.provider.N=...。适合全局生效的场景。
  • 动态注册:代码里调用 Security.addProvider(new MyProvider()) 或 Security.insertProviderAt()。我更推荐这种方式,因为它不会污染其他JVM实例。我在微服务项目中就吃过静态注册的亏——不同服务需要的Provider版本不一样,动态注册才是正解。
// 动态注册一个Provider
Security.addProvider(new BouncyCastleProvider());

// 查看当前所有已注册的Provider
Provider[] providers = Security.getProviders();
for (Provider p : providers) {
    System.out.println(p.getName() + " - " + p.getVersion());
}

// 获取某个Provider支持的算法列表
Provider p = Security.getProvider("BC");
Set<Object> keys = p.keySet();
for (Object key : keys) {
    if (key.toString().startsWith("Cipher.")) {
        System.out.println(key);
    }
}
注意:Provider 的注册顺序会影响算法的解析。如果你同时注册了多个Provider,且它们的某个算法名相同,JCA默认使用第一个能匹配的。我曾经把国密Provider排在SunJCE前面,导致SM4被正确解析;后来一个同事把顺序调反,SM4实际走了SunJCE的缺省算法——bug查了整整一下午。

2.4 Cipher —— 加密/解密

这个类很直接。你用 Cipher.getInstance("算法/模式/填充") 拿到实例,然后初始化模式(ENCRYPT_MODE 或 DECRYPT_MODE),传入密钥,接着调用 doFinal() 完成操作。

我在项目中踩过的坑

国密 SM4 的默认模式和填充跟 AES 不完全一样。SM4 常用的模式是 ECB 或 CBC,填充是 PKCS7Padding。但你直接写 "SM4/ECB/PKCS7Padding" 可能在有些Provider下不认识。实际上BouncyCastle和GMJCE都支持 "SM4/ECB/PKCS5Padding"——PKCS5Padding在Java里等价于PKCS7Padding。为此我专门写了个工具类统一处理算法字符串,免得每个开发都记混。

// SM4 加密示例
public static byte[] sm4Encrypt(byte[] key, byte[] data) throws Exception {
    SecretKeySpec spec = new SecretKeySpec(key, "SM4");
    Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, spec);
    return cipher.doFinal(data);
}

// SM4 解密
public static byte[] sm4Decrypt(byte[] key, byte[] encrypted) throws Exception {
    SecretKeySpec spec = new SecretKeySpec(key, "SM4");
    Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, spec);
    return cipher.doFinal(encrypted);
}

2.5 Signature —— 数字签名

签名流程:生成密钥对 → 用私钥签名 → 用公钥验签。Signature 类同样支持多Provider。

// SM2 签名
public static byte[] sm2Sign(PrivateKey privateKey, byte[] data) throws Exception {
    Signature sig = Signature.getInstance("SM3withSM2");
    sig.initSign(privateKey);
    sig.update(data);
    return sig.sign();
}

// SM2 验签
public static boolean sm2Verify(PublicKey publicKey, byte[] data, byte[] signature) throws Exception {
    Signature sig = Signature.getInstance("SM3withSM2");
    sig.initVerify(publicKey);
    sig.update(data);
    return sig.verify(signature);
}

你想想看,如果换成RSA,只需要把算法名改成 "SHA256withRSA",其他代码几乎一样。这就是Provider抽象层的好处。

2.6 MessageDigest —— 消息摘要

MessageDigest 是使用最简单的引擎类。它不需要密钥,初始化后直接 digest() 就拿到了哈希值。

// SM3 哈希
public static byte[] sm3(byte[] data) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SM3");
    return md.digest(data);
}

// 流式更新数据
public static byte[] sm3Stream(InputStream in) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SM3");
    byte[] buffer = new byte[4096];
    int len;
    while ((len = in.read(buffer)) != -1) {
        md.update(buffer, 0, len);
    }
    return md.digest();
}
核心知识点:
  • 所有引擎类都通过 getInstance(String algorithm) 获取实例,可以加Provider参数。
  • Provider 决定了算法实现的实际性能和安全强度。
  • 国密算法(SM2/SM3/SM4)需要依赖第三方Provider(BouncyCastle或GMJCE)。

2.7 JCA/JCE 架构图

下面这张SVG把整个调用链路画出来了。你看左边是应用代码,右边是实际的加密算法实现,中间是Provider抽象层——一切隔离都靠它。

JCA/JCE架构调用关系 应用程序 Cipher/Signature/MD Provider API getInstance() Provider Registry SunJCE BC (BouncyCastle) GMJCE ... 算法实现 SPI SM4Engine SM3Digest SM2Signer 说明: ① 应用通过标准API(Cipher/Signature)调用加解密。 ② API将请求路由到已注册的Provider,由Provider查找对应算法。 ③ 最终由Provider内部的SPI引擎完成实际计算。

嗯,到这里JCA/JCE的核心类就齐了。你掌握了Provider、Cipher、Signature、MessageDigest这四样,后面封装国密SDK的时候就已经有了地基。下个章节我们会把这些类串起来,写一个完整的国密工具类——你会看到,原来换一种Provider就能让整个应用支持SM2/SM3/SM4,而业务代码几乎不动。