彻底搞懂公钥加密的核心原理与实际应用

程序猿卫红 安全 阅读 2,853
赞 16 收藏
二维码
手机扫码查看
反馈

这次想说点实在的:前端做公钥加密,到底怎么选?

说实话,我一开始也没打算在前端搞什么公钥加密。毕竟大家都说“前端代码都能看,加密有啥用”,这话没错。但问题是,现在有些业务场景就是得在客户端先处理一点敏感数据,比如用户填写完表单后要提前加密某些字段再发给后端;或者要做本地存储加密,防止明文被直接读出来。这时候,哪怕不能完全防住逆向,至少也得提高门槛。

彻底搞懂公钥加密的核心原理与实际应用

所以我最近折腾了几种前端可用的公钥加密方案:Web Crypto API、Node.js crypto 模块(配合打包工具用在前端)、还有第三方库如 jsencryptforge。结论先放这儿:我现在基本只用 Web Crypto + forge 配合着来,其他的基本都踩过坑,要么太老,要么兼容性差到怀疑人生。

谁更灵活?谁更省事?

先说体验最差的:jsencrypt。这库曾经很火,基于 RSA 加密,封装得还挺简洁。但我现在是真不敢用了——它底层依赖的是已废弃的 jsbn 库,没类型定义,ES6 导入麻烦,最关键的是,不支持现代哈希算法填充(比如 RSA-PSS),而且对密钥格式要求死板,必须 PEM 格式还得带头尾标记。有一次我传了个标准 base64 的 public key,解码失败,折腾了半天发现是因为少了 -----BEGIN PUBLIC KEY----- 这种字符串……服了。

// jsencrypt 用法示例(我现在看到这种就想绕路)
import JSEncrypt from 'jsencrypt';

const encrypt = new JSEncrypt();
encrypt.setPublicKey(-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----);

const encrypted = encrypt.encrypt('hello world');

不是说它不能用,而是这种写法在现代项目里太格格不入了。尤其你要是用 Vite 或者 Webpack 5,Tree-shaking 可能还抽不掉冗余代码,体积也不小。我已经把它从所有新项目里清出去了。

Web Crypto API:原生才是王道

后来我转头去认真看了下浏览器自带的 Web Crypto API。说实话,第一次用的时候觉得文档反人类,API 设计像上个世纪的产物。但它最大的优势是:无需引入任何依赖,现代浏览器基本都支持(除了 IE 全家桶)。关键是安全模型更可信——毕竟是内置的,不会被篡改。

而且它支持的算法比你想得多:RSA-OAEP、ECDSA、AES-GCM 等等都有。虽然接口是 Promise-based 并且需要手动 import/export 密钥,但一旦熟悉套路,其实挺顺手的。

// 使用 Web Crypto API 进行 RSA-OAEP 加密
async function encryptWithPublicKey(data, publicKeyPem) {
  // 将 PEM 格式的公钥转换为 ArrayBuffer
  const binaryString = atob(publicKeyPem.replace(/-----.*-----/g, '').replace(/s/g, ''));
  const publicKeyRaw = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    publicKeyRaw[i] = binaryString.charCodeAt(i);
  }

  const importedKey = await crypto.subtle.importKey(
    'spki',
    publicKeyRaw,
    { name: 'RSA-OAEP', hash: 'SHA-256' },
    false,
    ['encrypt']
  );

  const encoder = new TextEncoder();
  const encodedData = encoder.encode(data);

  const encrypted = await crypto.subtle.encrypt(
    { name: 'RSA-OAEP' },
    importedKey,
    encodedData
  );

  return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}

注意这里的坑:Web Crypto 要求密钥是 ArrayBuffer 形式,所以你得自己把 PEM 解码成 DER 再喂进去。这个过程容易出错,尤其是换行符处理和 base64 解码方式。我当时就是因为用了错误的 atob 解码方式,在 Safari 上直接报错。后来改成用 TextDecoder 才稳下来。

还有一个限制:Web Crypto 的非对称加密只支持小数据块(RSA 通常是 100 多字节),所以大内容得用混合加密——用 RSA 加密一个随机 AES 密钥,再用 AES 加密正文。这套流程得你自己实现,没有一键封装。

Forge:妥协中的实用派

那有没有既能用现代构建工具,又不用自己写一大堆底层逻辑的?我最后找到了 node-forge。别看名字带 node,它其实可以在浏览器跑,通过 webpack 或 browserify 打包进来就行。

forge 的好处是:支持完整的 X.509、PKCS#8、PEM 解析,可以直接读字符串形式的密钥,而且提供了 high-level 接口。比如你可以直接 pki.publicKeyFromPem(),爽得很。

// 使用 forge 做 RSA 加密
import forge from 'node-forge';

function encryptWithForge(data, publicKeyPem) {
  const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
  const encrypted = publicKey.encrypt(data, 'RSA-OAEP', {
    md: forge.md.sha256.create(),
  });

  return forge.util.encode64(encrypted);
}

// 解密示例(一般在服务端)
function decryptWithPrivateKey(encryptedB64, privateKeyPem) {
  const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
  const encrypted = forge.util.decode64(encryptedB64);
  return privateKey.decrypt(encrypted, 'RSA-OAEP', {
    md: forge.md.sha256.create(),
  });
}

我现在的做法是:前端用 Web Crypto 处理简单的加密需求(比如 token 签名验证),复杂或需要兼容旧系统的就上 forge。虽然它体积大了点(gzip 后大概 80KB),但在很多企业级项目里这点代价是可以接受的。

特别提醒一点:如果你要用 fetch 请求获取公钥,记得确认响应的是纯文本而不是 JSON,不然解析 PEM 容易多一层转义。我就因为后端返回了个 JSON 包裹的 key 字段,结果忘了 JSON.parse 再取值,debug 了半小时才意识到问题。

我的选型逻辑

总结一下我的选择路径:

  • 如果只是轻量级加密、签名验证,且目标浏览器支持良好 → 优先选 Web Crypto API
  • 如果有复杂证书操作、密钥格式混乱、需要调试方便 → 上 forge
  • jsencrypt?除非维护老项目,否则建议早点替换掉
  • Node.js crypto 模块?别想了,打包进前端会炸体积,还可能触发 SSR 兼容问题

还有一个隐藏因素:团队协作。如果你的团队新人多,Web Crypto 学习成本高,那我反而会推荐先用 forge,文档全、例子多,至少不至于让同事卡在 importKey 报错上一整天。

另外提一嘴性能。我在 Chrome 120 上测试过,同样加密 1KB 数据,Web Crypto 平均耗时 2ms,forge 是 6~8ms。差距不大,但如果高频调用就得考虑了。不过实际业务中这种操作通常不会密集发生,所以我觉得可以忽略。

最后一点真实感受

前端做公钥加密这件事,本质上是在“有限的安全”和“可用性”之间找平衡。你永远防不住有心人调试内存、hook 函数,但至少能让自动化爬虫和初级攻击者知难而退。

我也试过 wasm 版的加密库,比如把 Rust 编译成 wasm 来跑 openssl 绑定,理论上更快更安全,但调试起来简直是噩梦。改个密钥格式要重新编译,开发效率断崖下跌,最终放弃。

所以目前最优解还是 Web Crypto + forge 搭配使用。哪个顺手用哪个,别追求完美方案。改完后仍有一两个小问题?只要不影响主流程,我就先上线再说。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类实战经验。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
瑞丹🍀
文章里的内容让我感受到了技术分享的价值,也让我更愿意去分享自己的经验。
点赞
2026-03-22 10:26