杰瑞科技汇

js java rsa 加密解密

核心概念与挑战

在开始之前,必须理解一个关键点:RSA 算法本身有长度限制,它不能直接加密任意长度的数据,只能加密一个“小块”(称为“模数”,modulus),一个 2048 位的 RSA 密钥,其最大加密数据块大小是 245 字节。

js java rsa 加密解密-图1
(图片来源网络,侵删)

在实际应用中,我们通常采用 “混合加密” 的模式:

  1. 前端 (JS):

    • 生成一个临时的、对称的 AES 密钥(256 位)。
    • 使用这个 AES 密钥 来加密实际的业务数据(JSON 字符串),这被称为“数据加密”。
    • 使用 RSA 公钥 来加密这个临时的 AES 密钥,这被称为“密钥加密”。
    • 加密后的 AES 密钥加密后的业务数据 一并发送给后端。
  2. 后端 (Java):

    • 使用 RSA 私钥 来解密出 AES 密钥
    • 使用解密出的 AES 密钥 来解密 业务数据,得到原始信息。

这种方式的优点是:

js java rsa 加密解密-图2
(图片来源网络,侵删)
  • 性能高: 对称加密(AES)比非对称加密(RSA)快得多。
  • 安全: 结合了两种加密的优点,既保证了密钥交换的安全(通过 RSA),又保证了数据传输的高效(通过 AES)。

准备工作:生成 RSA 密钥对

我们需要一个公钥和一个私钥,为了确保 JS 和 Java 能够兼容,最好使用 PEM 格式PKCS#1PKCS#8 标准。

你可以使用 OpenSSL 命令行工具来生成:

# 生成一个 2048 位的 RSA 私钥 (PKCS#8 格式,推荐,兼容性更好)
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# 从私钥生成公钥 (PKCS#1 格式)
openssl rsa -pubout -in private_key.pem -out public_key.pem

执行后,你会得到两个文件:

  • private_key.pem: 私钥必须严格保密,存放在后端服务器。
  • public_key.pem: 公钥,可以安全地提供给前端。

前端加密,后端解密

这是最常见的需求。

js java rsa 加密解密-图3
(图片来源网络,侵删)

后端 (Java) 准备

我们将使用 Java 的 java.security 包来处理 RSA 加密。

RsaCryptoUtil.java

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RsaCryptoUtil {
    // 从 PEM 格式的私钥字符串中提取私钥
    public static PrivateKey getPrivateKey(String privateKeyPem) throws Exception {
        privateKeyPem = privateKeyPem
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s", "");
        byte[] keyBytes = Base64.getDecoder().decode(privateKeyPem);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(spec);
    }
    // 从 PEM 格式的公钥字符串中提取公钥
    public static PublicKey getPublicKey(String publicKeyPem) throws Exception {
        publicKeyPem = publicKeyPem
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");
        byte[] keyBytes = Base64.getDecoder().decode(publicKeyPem);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(spec);
    }
    // RSA 公钥加密
    public static String encryptByPublicKey(String data, String publicKeyPem) throws Exception {
        PublicKey publicKey = getPublicKey(publicKeyPem);
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }
    // RSA 私钥解密
    public static String decryptByPrivateKey(String encryptedData, String privateKeyPem) throws Exception {
        PrivateKey privateKey = getPrivateKey(privateKeyPem);
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }
}

前端 (JavaScript) 准备

我们将使用 jsencrypt 这个流行的库,它简化了 RSA 的操作。

安装 jsencrypt

npm install jsencrypt
# 或者
yarn add jsencrypt
# 如果在 HTML 中使用,可以直接通过 CDN 引入
# <script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js"></script>

前端代码示例 (encrypt.js)

import JSEncrypt from 'jsencrypt';
// 从你的 public_key.pem 文件中读取公钥内容
const publicKeyPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy... (你的公钥内容) ...QIDAQAB
-----END PUBLIC KEY-----`;
// 1. 创建 JSEncrypt 实例
const encrypt = new JSEncrypt();
// 2. 设置公钥
encrypt.setPublicKey(publicKeyPem);
// 3. 要加密的数据
const originalData = "这是一条来自前端的秘密信息!";
// 4. 使用公钥加密
// 注意:jsencrypt 会自动进行 Base64 编码
const encryptedData = encrypt.encrypt(originalData);
if (encryptedData) {
    console.log("原始数据:", originalData);
    console.log("加密后的数据 (Base64):", encryptedData);
    // 在实际应用中,你会将 encryptedData 发送到后端
    // 这里我们模拟发送,并调用后端的解密方法
    simulateBackendRequest(encryptedData);
} else {
    console.error("加密失败!请检查密钥或数据长度。");
}
// 模拟发送请求到后端
async function simulateBackendRequest(encryptedData) {
    // 在真实项目中,这里会是 fetch 或 axios 请求
    console.log("\n--- 模拟发送到后端 ---");
    console.log("后端收到的加密数据:", encryptedData);
    // 假设后端解密方法已经准备好
    // 你需要一个 Node.js 环境来运行下面的 Java 代码,或者通过 API 调用你的后端服务
    // 这里我们只是打印出后端应该调用的方法
    console.log("\n后端 Java 应该执行:");
    console.log("String decryptedData = RsaCryptoUtil.decryptByPrivateKey(\"" + encryptedData + "\", \"...你的私钥...\");");
    console.log("System.out.println(\"解密后的数据: \" + decryptedData);");
}

验证流程

  1. 前端: 加密 originalData 得到 encryptedData (一个 Base64 字符串)。
  2. 前端: 将 encryptedData 发送到后端的某个 API 端点。
  3. 后端: 接收到 encryptedData
  4. 后端: 调用 RsaCryptoUtil.decryptByPrivateKey(encryptedData, privateKeyPem) 方法。
  5. 后端: 得到解密后的 originalData,可以正常处理。

后端加密,前端解密

这个场景相对少见,通常用于后端向前端下发一些需要保密的配置或令牌,流程与场景一相反。

后端 (Java) 加密

后端使用 公钥 加密数据,然后将加密数据和 私钥 一同发送给前端。注意:将私钥发送给前端存在极高的安全风险,请务必谨慎! 通常只在特定且受信任的内部场景下使用。

// ... (RsaCryptoUtil 类同上)
public class BackendEncryption {
    public static void main(String[] args) throws Exception {
        // 从文件中读取公钥和私钥
        String publicKeyPem = new String(Files.readAllBytes(Paths.get("public_key.pem")));
        String privateKeyPem = new String(Files.readAllBytes(Paths.get("private_key.pem")));
        String dataToEncrypt = "这是后端生成的秘密配置";
        // 1. 后端使用公钥加密数据
        String encryptedData = RsaCryptoUtil.encryptByPublicKey(dataToEncrypt, publicKeyPem);
        System.out.println("后端加密后的数据 (Base64): " + encryptedData);
        // 2. (高风险操作) 将加密数据和私钥发送给前端
        System.out.println("\n--- 发送给前端的数据 ---");
        System.out.println("加密数据: " + encryptedData);
        System.out.println("私钥 (PEM格式): \n" + privateKeyPem);
    }
}

前端 (JavaScript) 解密

前端需要同时接收加密数据和私钥。

import JSEncrypt from 'jsencrypt';
// 假设从后端接收到的数据
const receivedEncryptedData = "..."; // 后端加密后的 Base64 数据
const receivedPrivateKeyPem = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... (你的私钥内容) ...AgMBAAE
-----END PRIVATE KEY-----`;
// 1. 创建 JSEncrypt 实例
const decrypt = new JSEncrypt();
// 2. 设置私钥
decrypt.setPrivateKey(receivedPrivateKeyPem);
// 3. 使用私钥解密
// jsencrypt 的 decrypt 方法接收 Base64 编密的字符串,并返回原始字符串
const decryptedData = decrypt.decrypt(receivedEncryptedData);
if (decryptedData) {
    console.log("前端收到的加密数据:", receivedEncryptedData);
    console.log("解密后的数据:", decryptedData);
} else {
    console.error("解密失败!请检查私钥或加密数据。");
}

总结与最佳实践

场景 加密方 解密方 关键点
前端 -> 后端 前端 (使用公钥) 后端 (使用私钥) 最常见,公钥可公开,私钥在后端保密。
后端 -> 前端 后端 (使用公钥) 前端 (使用私钥) 风险较高,需要将私钥安全地传递给前端,不推荐在生产环境使用。

重要注意事项:

  1. 密钥管理: 私钥是安全的生命线,绝对不能泄露,妥善存储,最好使用密钥库或专门的密钥管理服务。
  2. 填充模式: 我们使用了 PKCS1Padding,这是最常用的填充模式,前后端必须保持一致。
  3. 数据长度: 如果直接加密大文件或长文本,务必使用上面提到的 “混合加密” 方案(RSA + AES),直接用 RSA 加密长数据会失败。
  4. 字符编码: 前后端统一使用 UTF-8 编码,避免因编码不一致导致乱码。
  5. Base64: RSA 加密后的二进制数据通常不能直接用于网络传输或存储,需要用 Base64 进行编码。jsencrypt 和 Java 的 Base64 类都方便地处理了这一点。
分享:
扫描分享到社交APP
上一篇
下一篇