小易说IT 小易说IT

RSA+AES 混合加密 生产版 Java 示例

特性:

  1. AES:AES-256-GCM(带认证,同时实现加密 + 完整性校验,比 CBC 更安全,自带 IV、AuthTag)

  2. RSA:RSA/ECB/OAEPWithSHA-256AndMGF1Padding 加密 AES 密钥

  3. 签名:SHA256withRSA(可选,可单独对业务数据签名,防抵赖)

  4. 输出统一封装成对象,序列化用 Base64,方便接口传输(JSON)

  5. JDK 原生 API,无第三方依赖;JDK8 注意:AES-256 需要额外安装无限制权限包;JDK9 + 默认支持

  6. 流程:

发送方:随机生成 AES 密钥 + IV → AES-GCM 加密业务数据 → RSA 加密 AES 密钥 → (可选 RSA 签名原始报文) 接收方:RSA 解密拿到 AES 密钥 → AES-GCM 解密数据(GCM 会自动校验篡改)

说明:GCM 模式会输出 密文 + tag,tag 是认证标签,解密失败 = 数据被篡改 / 密钥错误。

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * RSA + AES-GCM 混合加密工具类(生产参考模型)
 * AES: AES-256-GCM
 * RSA: RSA/ECB/OAEPWithSHA-256AndMGF1Padding (2048bit)
 */
public class RsaAesMixUtil {

    // ========= 常量定义 =========
    private static final String RSA_ALG = "RSA";
    private static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
    private static final String AES_ALG = "AES";
    private static final String AES_CIPHER = "AES/GCM/NoPadding";
    // GCM 参数:tag长度 128bit,推荐IV固定12字节(NIST推荐)
    private static final int GCM_TAG_LEN_BIT = 128;
    private static final int GCM_IV_LEN = 12;
    private static final int AES_KEY_SIZE = 256;
    private static final int RSA_KEY_SIZE = 2048;
    private static final String SIGN_ALG = "SHA256withRSA";

    /**
     * 加密结果实体,接口传输转JSON
     */
    public static class EncryptResult {
        // RSA加密后的AES密钥(Base64)
        public String encryptedAesKey;
        // AES IV(Base64)
        public String iv;
        // AES加密业务密文(Base64)
        public String cipherText;
        // GCM auth tag(Base64)
        public String tag;
        // 可选:原始报文签名(Base64),不需要签名可以去掉
        public String sign;
    }

    /**
     * 生成RSA密钥对
     */
    public static KeyPair generateRsaKeyPair() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance(RSA_ALG);
        gen.initialize(RSA_KEY_SIZE);
        return gen.generateKeyPair();
    }

    /**
     * 生成AES 256密钥
     */
    public static SecretKey generateAesKey() throws Exception {
        KeyGenerator gen = KeyGenerator.getInstance(AES_ALG);
        gen.init(AES_KEY_SIZE);
        return gen.generateKey();
    }

    /**
     * RSA公钥加密(用来加密AES密钥)
     */
    public static byte[] rsaEncrypt(byte[] data, PublicKey pubKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_CIPHER);
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
        return cipher.doFinal(data);
    }

    /**
     * RSA私钥解密(还原AES密钥)
     */
    public static byte[] rsaDecrypt(byte[] cipherData, PrivateKey priKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_CIPHER);
        cipher.init(Cipher.DECRYPT_MODE, priKey);
        return cipher.doFinal(cipherData);
    }

    /**
     * AES-GCM加密
     * @param plain 原始业务字节
     * @param aesKey AES密钥
     * @return [cipherBytes, tagBytes]
     */
    public static byte[][] aesGcmEncrypt(byte[] plain, SecretKey aesKey) throws Exception {
        // 随机IV
        byte[] iv = new byte[GCM_IV_LEN];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);

        Cipher cipher = Cipher.getInstance(AES_CIPHER);
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BIT, iv);
        cipher.init(Cipher.ENCRYPT_MODE, aesKey, spec);
        byte[] cipherText = cipher.doFinal(plain);
        byte[] tag = cipher.getIV();
        // GCM模式:doFinal输出=密文+tag,拆分:最后tag长度是16字节(128bit)
        byte[] realCipher = new byte[cipherText.length - (GCM_TAG_LEN_BIT / 8)];
        byte[] authTag = new byte[GCM_TAG_LEN_BIT / 8];
        System.arraycopy(cipherText, 0, realCipher, 0, realCipher.length);
        System.arraycopy(cipherText, realCipher.length, authTag, 0, authTag.length);
        return new byte[][]{iv, realCipher, authTag};
    }

    /**
     * AES-GCM解密(自动校验tag,篡改直接抛异常)
     */
    public static byte[] aesGcmDecrypt(byte[] cipherBytes, byte[] iv, byte[] tag, SecretKey aesKey) throws Exception {
        Cipher cipher = Cipher.getInstance(AES_CIPHER);
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BIT, iv);
        cipher.init(Cipher.DECRYPT_MODE, aesKey, spec);
        // 拼接密文+tag
        byte[] input = new byte[cipherBytes.length + tag.length];
        System.arraycopy(cipherBytes, 0, input, 0, cipherBytes.length);
        System.arraycopy(tag, 0, input, cipherBytes.length, tag.length);
        return cipher.doFinal(input);
    }

    /**
     * RSA私钥签名
     */
    public static byte[] sign(byte[] data, PrivateKey priKey) throws Exception {
        Signature signature = Signature.getInstance(SIGN_ALG);
        signature.initSign(priKey);
        signature.update(data);
        return signature.sign();
    }

    /**
     * RSA公钥验签
     */
    public static boolean verify(byte[] data, byte[] signBytes, PublicKey pubKey) throws Exception {
        Signature signature = Signature.getInstance(SIGN_ALG);
        signature.initVerify(pubKey);
        signature.update(data);
        return signature.verify(signBytes);
    }

    // ========= 密钥序列化工具(Base64互转) =========
    public static String pub2Base64(PublicKey pub) {
        return Base64.getEncoder().encodeToString(pub.getEncoded());
    }

    public static String pri2Base64(PrivateKey pri) {
        return Base64.getEncoder().encodeToString(pri.getEncoded());
    }

    public static PublicKey base64ToPub(String base64) throws Exception {
        byte[] b = Base64.getDecoder().decode(base64);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(b);
        return KeyFactory.getInstance(RSA_ALG).generatePublic(spec);
    }

    public static PrivateKey base64ToPri(String base64) throws Exception {
        byte[] b = Base64.getDecoder().decode(base64);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(b);
        return KeyFactory.getInstance(RSA_ALG).generatePrivate(spec);
    }

    // ========= 对外统一加密入口 =========
    public static EncryptResult encrypt(String plainText, PublicKey receiverPubKey, PrivateKey senderPriKey) throws Exception {
        EncryptResult result = new EncryptResult();
        byte[] plainBytes = plainText.getBytes();
        // 1. 随机AES密钥
        SecretKey aesKey = generateAesKey();
        // 2. AES加密业务数据
        byte[][] gcmRet = aesGcmEncrypt(plainBytes, aesKey);
        byte[] iv = gcmRet[0];
        byte[] cipher = gcmRet[1];
        byte[] tag = gcmRet[2];
        // 3. RSA加密AES密钥(用接收方公钥)
        byte[] encryptedAesKey = rsaEncrypt(aesKey.getEncoded(), receiverPubKey);
        // 4. 签名原始报文(发送方私钥,可选)
        byte[] signBytes = sign(plainBytes, senderPriKey);

        result.encryptedAesKey = Base64.getEncoder().encodeToString(encryptedAesKey);
        result.iv = Base64.getEncoder().encodeToString(iv);
        result.cipherText = Base64.getEncoder().encodeToString(cipher);
        result.tag = Base64.getEncoder().encodeToString(tag);
        result.sign = Base64.getEncoder().encodeToString(signBytes);
        return result;
    }

    // ========= 对外统一解密入口 =========
    public static String decrypt(EncryptResult encryptResult, PrivateKey receiverPriKey, PublicKey senderPubKey) throws Exception {
        // 1. Base64解码
        byte[] encryptedAesKey = Base64.getDecoder().decode(encryptResult.encryptedAesKey);
        byte[] iv = Base64.getDecoder().decode(encryptResult.iv);
        byte[] cipher = Base64.getDecoder().decode(encryptResult.cipherText);
        byte[] tag = Base64.getDecoder().decode(encryptResult.tag);
        byte[] signBytes = Base64.getDecoder().decode(encryptResult.sign);

        // 2. RSA解密拿到AES密钥
        byte[] aesKeyRaw = rsaDecrypt(encryptedAesKey, receiverPriKey);
        SecretKey aesKey = new SecretKeySpec(aesKeyRaw, AES_ALG);
        // 3. AES GCM解密
        byte[] plainBytes = aesGcmDecrypt(cipher, iv, tag, aesKey);
        // 4. 验签(校验发送方身份+报文未篡改)
        boolean pass = verify(plainBytes, signBytes, senderPubKey);
        if (!pass) {
            throw new SecurityException("签名校验失败,数据来源不可信或报文被篡改");
        }
        return new String(plainBytes);
    }

    public static void main(String[] args) throws Exception {
        // ===== 角色:接收方(服务端)RSA密钥对,用来解密AES密钥 =====
        KeyPair receiverKp = generateRsaKeyPair();
        PublicKey receiverPub = receiverKp.getPublic();
        PrivateKey receiverPri = receiverKp.getPrivate();

        // ===== 角色:发送方(客户端)RSA密钥对,用来签名 =====
        KeyPair senderKp = generateRsaKeyPair();
        PublicKey senderPub = senderKp.getPublic();
        PrivateKey senderPri = senderKp.getPrivate();

        String originMsg = "这是业务明文,可以很长,RSA只加密AES密钥";
        System.out.println("原始报文:" + originMsg);

        // 加密
        EncryptResult encryptResult = encrypt(originMsg, receiverPub, senderPri);
        System.out.println("\nencryptedAesKey:" + encryptResult.encryptedAesKey);
        System.out.println("iv:" + encryptResult.iv);
        System.out.println("cipherText:" + encryptResult.cipherText);
        System.out.println("tag:" + encryptResult.tag);
        System.out.println("sign:" + encryptResult.sign);

        // 解密
        String decrypted = decrypt(encryptResult, receiverPri, senderPub);
        System.out.println("\n解密结果:" + decrypted);

        // 模拟篡改测试:修改cipherText,GCM解密直接抛异常
        // encryptResult.cipherText = "xxx";
        // decrypt(encryptResult, receiverPri, senderPub);
    }
}

架构说明(接口交互)

客户端(发送方)
1. 随机AES-256密钥
2. AES-GCM加密业务报文
3. 【接收方公钥】加密AES密钥(保证只有接收方能解开AES key)
4. 【发送方私钥】对原始报文签名(证明是我发的,不可抵赖)
→ 传给后端JSON:{encryptedAesKey,iv,cipherText,tag,sign}

服务端(接收方)
1. 自己私钥解密得到AES密钥
2. AES-GCM解密(tag校验,防篡改)
3. 使用【发送方公钥】验签
4. 验签通过才使用业务数据

⚠️ 生产落地清单(重点)

  1. 密钥分发

    • 接收方公钥公开给调用方;接收方私钥严格保管,放 KMS,不要写配置文件

    • 发送方私钥仅自己持有;发送方公钥提前给接收方(或者放证书)

  2. JDK 版本兼容

    • JDK8:AES-256 会抛 IllegalKeySizeException,需要安装 JCE Unlimited Strength Jurisdiction Policy Files

    • JDK 1.8.161+ / JDK9+ 默认支持 256 位 AES,不需要额外包

  3. GCM 注意:IV 必须每次随机,不要复用同一个 IV + 同一个 AES 密钥,会严重破坏安全性

  4. 异常处理:

    • GCM 解密失败、验签失败统一捕获,不要对外返回详细错误堆栈,只返回 “数据非法”

    • 不要区分是解密失败还是验签失败(防止给攻击者提供侧信道信息)

  5. 关于签名:

    • 如果只做 HTTPS 传输、不需要业务层不可抵赖,可以把签名逻辑删掉,减少性能损耗

    • 需要更高安全的签名,把 SHA256withRSA 替换成 SHA256withRSAandMGF1(RSASSA-PSS)

  6. 长度:AES 可以加密超大文本,不受 RSA 长度限制;RSA 只固定加密 32 字节 AES 密钥

  7. 编码:接口传输全部 Base64,不要直接传二进制数组

扩展方案选择

  • 如果对接第三方老系统(只支持 PKCS1):RSA 填充改为 RSA/ECB/PKCS1Padding

  • 如果需要国密:替换成 SM2 + SM4

  • 高并发场景:AES 生成、SecureRandom 可以做少量池化优化(但 SecureRandom 本身不要随便改)