Java RSA加解密怎么用才安全

RSA 代码看起来只是生成密钥、调用 Cipher、再做 Base64,但密钥长度、填充模式、明文长度和私钥保存方式任意一处选错,都可能留下安全隐患。这篇从 RSA 的适用边界讲起,用 Java 示例说明 RSA 2048、OAEP SHA-256、X.509/PKCS#8 和混合加密应该怎么落地。

之前写过可逆(对称和非对称)与不可逆加密算法,那篇主要讲不同加密方式各自解决什么问题。

这篇往下落一点,专门看 Java 里 RSA 到底怎么实现。

RSA 的 API 并不复杂:生成密钥对、使用公钥加密、使用私钥解密。

真正容易出问题的地方,反而是那些看起来不起眼的选择:

  • 密钥长度是不是太短
  • 填充模式是不是还在使用旧方案
  • 是否拿 RSA 直接加密一大段配置或 JSON
  • Base64 是不是被误当成加密
  • 私钥是不是被硬编码进代码或配置文件
  • 公钥和私钥的编码格式是不是匹配

旧代码里常见的 1024 位 RSA 和 RSA/ECB/PKCS1Padding,不应该继续作为新项目的默认方案。

本文按当前更稳妥的方向重构示例。

先给结论:RSA 适合加密短数据或保护对称密钥,不适合直接加密大段业务正文;新代码优先使用 RSA 2048 及以上和 OAEP SHA-256,并把私钥放到专门的密钥管理位置。

RSA 解决什么问题

RSA 是非对称加密算法,使用一对密钥:

  • 公钥:可以公开
  • 私钥:只能由密钥持有方保存

最常见的加密方向是:

发送方拿到公钥
       │
       ▼
使用公钥加密数据
       │
       ▼
只有持有私钥的一方可以解密
​

签名方向则相反:

发送方使用私钥签名
       │
       ▼
其他人使用公钥验证签名
​

加密解决的是保密性,签名解决的是来源认证和完整性。

不要把“用私钥加密、公钥解密”简单当成普通加密的反向写法。实际工程中,这通常应该使用签名 API,而不是自己把签名流程写成一套加密流程。

RSA 和对称加密的区别

对称加密只有一把共享密钥,速度快,适合大数据量:

同一把密钥:加密 <-> 解密
​

RSA 使用公钥和私钥,密钥分发更方便,但计算成本高,而且单次可加密数据长度有限。

RSA:适合密钥交换、短数据、签名
AES-GCM:适合正文、文件和大数据量
​

真实系统通常使用混合加密:RSA 保护 AES 密钥,AES-GCM 加密正文。

密钥长度和填充模式

不要继续使用 1024 位

旧示例里经常能看到:

keyPairGenerator.initialize(1024);
​

1024 位 RSA 不适合作为新系统默认配置。

当前更常见的起点是 RSA 2048;如果系统生命周期很长或安全基线要求更高,可以评估 3072 位或更高。

密钥越长,计算成本也会增加,所以最终要结合协议、调用频率和部署环境测试。

PKCS1Padding 和 OAEP

RSA 不能直接把任意明文塞进模数里,还需要填充方案。

旧代码常见:

Cipher.getInstance("RSA/ECB/PKCS1Padding");
​

对于新设计,更推荐使用 OAEP,并明确哈希算法:

Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
​

但只看 transformation 名字还不够。不同 JDK 或安全 Provider 对 MGF1 默认哈希的处理可能有差异,生产代码最好显式传入 OAEPParameterSpec。

新项目建议:RSA 2048+ + OAEP + SHA-256
旧 PKCS#1 v1.5:只在兼容既有协议时保留,并完成风险评估
​

不要把 ECB 理解成 RSA 的分块模式。

Java 的 RSA transformation 习惯上写成 RSA/ECB/...,这里的 ECB 只是 API 命名格式,不代表 RSA 像 AES 那样使用 ECB 分组模式。

生成 RSA 密钥对

下面是一个只负责生成密钥对的示例。

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.util.Base64;

public final class RsaKeys {

    private RsaKeys() {
    }

    public static KeyPair generate() throws Exception {
        KeyPairGenerator generator =
                KeyPairGenerator.getInstance("RSA");

        // 新项目至少从 2048 位开始,具体按安全基线评估
        generator.initialize(2048, new SecureRandom());
        return generator.generateKeyPair();
    }

    public static void printForLocalTest(KeyPair keyPair) {
        // 只用于本地测试,不要把私钥打印到生产日志
        String publicKey = Base64.getEncoder()
                .encodeToString(keyPair.getPublic().getEncoded());
        String privateKey = Base64.getEncoder()
                .encodeToString(keyPair.getPrivate().getEncoded());

        System.out.println(publicKey);
        System.out.println(privateKey);
    }
}
​

公钥通常编码为 X.509 SubjectPublicKeyInfo。

私钥通常编码为 PKCS#8。

getEncoded() 得到的是 DER 二进制,Base64 只是为了方便放进文本配置或传输,Base64 不是加密。

如果密钥用于生产,不要把 privateKey 写进 Git 仓库、镜像层、普通配置文件或启动日志。

应放到 Secret、KMS、HSM 或受控的密钥管理服务中,并限制读取权限。

加载公钥和私钥

从 Base64 文本恢复密钥时,要使用匹配的 KeySpec:

import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public final class RsaKeyLoader {

    private RsaKeyLoader() {
    }

    public static PublicKey loadPublicKey(String encoded)
            throws Exception {
        byte[] der = Base64.getMimeDecoder()
                .decode(encoded);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(new X509EncodedKeySpec(der));
    }

    public static PrivateKey loadPrivateKey(String encoded)
            throws Exception {
        byte[] der = Base64.getMimeDecoder()
                .decode(encoded);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(new PKCS8EncodedKeySpec(der));
    }
}
​

如果密钥文本来自 PEM 文件,通常还要先去掉头尾标记和空白:

-----BEGIN PUBLIC KEY-----
...Base64...
-----END PUBLIC KEY-----
​

实际代码需要根据 PEM 类型选择正确的解析方式。

PUBLIC KEY 通常对应 X.509 SubjectPublicKeyInfo,PRIVATE KEY 通常对应 PKCS#8;不要看到都是 Base64 就直接互换。

RSA 短数据加解密

下面给出一个 OAEP SHA-256 的短文本示例。

import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.util.Base64;

public final class RsaCrypto {

    private static final String TRANSFORMATION =
            "RSA/ECB/OAEPPadding";

    private static final OAEPParameterSpec OAEP_SHA256 =
            new OAEPParameterSpec(
                    "SHA-256",
                    "MGF1",
                    MGF1ParameterSpec.SHA256,
                    PSource.PSpecified.DEFAULT);

    private RsaCrypto() {
    }

    public static String encrypt(String plainText, PublicKey publicKey)
            throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey, OAEP_SHA256);

        byte[] plainBytes = plainText
                .getBytes(StandardCharsets.UTF_8);
        byte[] encrypted = cipher.doFinal(plainBytes);

        return Base64.getEncoder().encodeToString(encrypted);
    }

    public static String decrypt(String cipherText, PrivateKey privateKey)
            throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEP_SHA256);

        byte[] encrypted = Base64.getDecoder().decode(cipherText);
        byte[] plainBytes = cipher.doFinal(encrypted);

        return new String(plainBytes, StandardCharsets.UTF_8);
    }
}
​

这里有三个细节:

  • 加密和解密必须使用相同的 OAEP 参数
  • 明文和解密结果都明确使用 UTF-8
  • Base64 只负责把二进制密文转成文本

如果加密端和解密端的 OAEP 参数不一致,常见结果就是 BadPaddingException 或解密失败。

RSA 明文长度有限

RSA 能加密的最大明文长度和密钥长度、填充方式有关。

对于 RSA 2048 和 OAEP SHA-256,最大长度不是 2048 字节,而大约是:

模数长度字节数 - 2 × 哈希长度 - 2
= 256 - 2 × 32 - 2
= 190 字节
​

实际长度还要按具体 Provider 和参数计算,不能把某个固定数字写死到所有场景。

因此,配置文件、JSON、文件内容和长文本不应该直接用 RSA 加密。

把明文强行切片后分别 RSA 加密也不是首选方案,容易引入分片顺序、完整性校验、随机性和错误恢复问题。

大数据使用混合加密

处理长文本或文件时,推荐混合加密:

1. 随机生成 AES 会话密钥
2. 使用 AES-GCM 加密真正的业务数据
3. 使用 RSA 公钥加密 AES 会话密钥
4. 保存 RSA 密文、AES 密文、nonce 和必要版本信息

接收方:
1. 使用 RSA 私钥解密 AES 会话密钥
2. 使用 AES-GCM 和 nonce 解密业务数据
3. 校验 GCM 认证标签,失败就拒绝使用明文
​

整体结构可以表示为:

业务明文 ──AES-GCM──> 数据密文
                         │
随机 AES key ──RSA 公钥──> key 密文

最终传输:key 密文 + 数据密文 + nonce + 版本信息
​

AES-GCM 同时提供机密性和完整性校验,比自己使用 AES-CBC 再拼一个哈希更不容易出错。

RSA 只保护短小的 AES key,性能和长度问题就分开解决了。

如果项目已经使用成熟的 JOSE/JWE、云 KMS 或密码库,优先使用标准实现,不要手写一套私有协议。

签名不要当加密用

如果需求是“确认数据是谁发的,并且内容没有被改”,应该使用数字签名。

发送方:私钥签名
接收方:公钥验签
​

Java 中常见的是 Signature API,而不是把明文用私钥调用 Cipher 加密。

签名通常对摘要签名,不会把完整大文件直接塞进 RSA。

Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data);
byte[] signBytes = signature.sign();
​

验签方使用公钥:

Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(publicKey);
verifier.update(data);
boolean valid = verifier.verify(signBytes);
​

签名算法、密钥用途和加密算法应该在协议里明确写清楚,不能看到 RSA 就什么都用 RSA。

常见异常怎么排查

InvalidKeySpecException

通常和密钥格式不匹配、Base64 内容损坏、PEM 头尾没有处理、X.509/PKCS#8 用错有关。

排查顺序可以是:

  1. 确认密钥文本没有多余前缀、截断或错误转义
  2. 确认公钥使用 X509EncodedKeySpec
  3. 确认私钥使用 PKCS8EncodedKeySpec
  4. 确认 Base64 解码器和编码端规则一致
  5. 不要把公钥当私钥,或把 PKCS#1 私钥直接当 PKCS#8 读取

BadPaddingException

常见原因包括:

  • 加密和解密使用了不同填充模式
  • OAEP 的摘要或 MGF1 参数不一致
  • 使用了错误的私钥
  • 密文被截断或 Base64 内容被改动
  • 把签名数据当成加密数据解密

不要为了“让它解密成功”直接换成更弱的填充模式。

先确认协议双方的算法参数和密钥用途。

数据过长

RSA 加密长文本时,常见异常是输入数据过长。

正确方向不是随意调整字符串或强行扩大 RSA 密钥,而是改用混合加密,或者只用 RSA 包装一个短密钥。

密钥保存和轮换

安全实现不只在 Cipher 代码里。

私钥保存方式同样重要:

  • 不要写死在 Java 源码里
  • 不要提交到 Git 仓库
  • 不要打印到启动日志或异常日志
  • 不要和业务配置一起明文分发
  • 生产环境使用 Secret、KMS、HSM 或专用密钥服务
  • 建立密钥版本和轮换策略
  • 解密时根据密钥版本选择对应私钥

如果必须从环境变量读取,也要注意环境变量可能被诊断工具、进程信息或部署日志暴露。

更稳妥的架构是:应用只在需要时向密钥服务请求解密能力,不让长期私钥散落在每台应用机器上。

小结

Java RSA 加解密最重要的不是记住几段 API,而是先把算法边界想清楚:

  • 新项目优先 RSA 2048 及以上,具体按安全基线评估
  • 新协议优先 OAEP,并明确 SHA-256 等参数
  • 公钥通常使用 X.509,私钥通常使用 PKCS#8
  • Base64 是编码,不是加密
  • RSA 只适合短数据或保护对称密钥,不适合直接加密大文件和长 JSON
  • 大数据使用 AES-GCM 加密正文,再用 RSA 包装 AES key
  • 身份认证和完整性校验使用签名,不要把签名和加密混用
  • 私钥不进源码、不进日志、不进仓库,并且要支持撤销和轮换

一句话总结:RSA 的 API 很短,安全协议却很长;密钥长度、填充、数据长度、密钥保存和轮换,任何一环都不能只凭旧代码照抄。

如果只是学习 API,可以运行短文本示例;如果要保护真实配置或文件,先确认项目是否应该使用成熟的混合加密或 KMS 方案。

更多推荐

章节目录