ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

jsencrypt前端RSA加密实战:解决登录密码明文传输与uniapp兼容

jsencrypt前端RSA加密实战:解决登录密码明文传输与uniapp兼容 简介这是一份面向前端开发者的RSA加密解密工具包专门解决在uniapp等环境中使用jsencrypt时报错的问题。资源提供了修改后的jsencrypt加密库文件可在uni-app项目内正常引入同时包含封装好的RSA工具文件导出了加密rsaEncrypt和解密rsaDecrypt两个方法开发者只需简单调用即可对重要信息进行RSA加密后传给服务器也支持对服务器返回的密文进行解密。包内共3个文件以2个JS文件为主其中之一为兼容uni-app的加密库另一个为封装好的工具模块另有1个txt说明文档用于记录使用要点或密钥示例整体大小仅37KB轻量易集成。对于在公众号、小程序或App端需要实现前后端数据安全传输的开发者这份资源能省去大量踩坑时间。资源源自真实项目实战总结并附有在线生成公钥私钥的指引目前已有2805人学习下载适合具备一定前端基础、希望快速在项目中落地RSA加密的读者使用。1. 前端为什么要用 jsencrypt 做 RSA不解决这个登录传参就一直在裸奔做前端开发几年你迟早会遇到一类需求登录密码、手机号、身份证号这些敏感字段明文塞进 POST body 里发给后端。运气好走的是 HTTPS运气不好 HTTP 环境下抓包直接能看到原文。jsencrypt 是前端做 RSA 加密最常用、也几乎是唯一被验证过可用性的库它让浏览器端用公钥加密、后端用私钥解密密文即使被截走也解不开。更关键的是它不光在普通网页里能用uniapp 的 H5 端和 App 端同样跑得通小程序端做点兼容处理也能用。适合谁适合正在写登录注册、支付确认、实名认证这类页面的前端或者团队里后端已经生成好 RSA 密钥对、只等前端把加密接上的情况。下面这套流程我拆成可直接照抄的步骤来讲从密钥格式到分段加密再到底层踩过的坑。2. RSA 的密钥与格式先把公钥、私钥和填充方式理清楚2.1 安装与引入npm 装好后 uniapp 里怎么用jsencrypt 是一个纯 JavaScript 实现的 RSA 加解密库底层用到了bignumber和jsbn这两个数学计算库。它的 API 极其简单核心方法就两个encrypt()和decrypt()。先看一下最常规的安装方式npm install jsencrypt --save装完之后普通 Vue 项目或者 uniapp 的 H5 端直接在需要的页面里按需引入就行// 按需引入不要全局挂载到 Vue.prototype 上 import JSEncrypt from jsencrypt const encryptor new JSEncrypt() encryptor.setPublicKey(-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----) const encrypted encryptor.encrypt(123456) console.log(encrypted)这段代码里做了三件事创建JSEncrypt实例、设置公钥、调用encrypt()加密。逻辑上一步对应一个方法没有多余的动作。需要注意encrypt()返回的是 Base64 编码后的密文字符串不是二进制数组所以传输的时候可以直接放进 JSON 字段或者 URL 参数里后端拿到的也是这个 Base64 字符串。如果你是 uniapp 项目还有一条更省事的路径——把node_modules/jsencrypt/bin/jsencrypt.min.js拷贝到项目的common目录下然后通过import或直接引静态文件的方式加载。这样做的原因是 jsencrypt 内部有对window对象的引用在小程序环境里window不存在直接打包可能会抛window is not defined。放在common目录下手动管理方便在需要的地方做条件编译。2.2 PKCS#1 与 PKCS#8公钥格式决定 encrypt 能不能返回正确密文很多人在第一步就翻车症状是encrypt(abc)返回false或者null。排查到最后九成是公钥格式不对。RSA 公钥有两种常见编码格式格式头尾标记常见来源PKCS#1-----BEGIN RSA PUBLIC KEY-----部分 Java 老代码、openssl genrsa 默认输出PKCS#8-----BEGIN PUBLIC KEY-----Java 新代码、Python、多数云厂商jsencrypt 底层走的是 ASN.1 解析对两种格式都能识别但前提是头尾标记必须完整。如果你从后端接口拿到的是纯 Base64 字符串没有头尾直接塞进去就会解析失败。我一般会先做一次格式化再使用function formatPublicKey(key) { // 如果已经带 BEGIN 标记就直接用 if (key.indexOf(BEGIN) ! -1) { return key } // 去掉所有空格和换行再拼上 PKCS#8 的标准头尾 const cleanKey key.replace(/\s/g, ) const lines cleanKey.match(/.{1,64}/g) return -----BEGIN PUBLIC KEY-----\n lines.join(\n) \n-----END PUBLIC KEY----- }这段格式化函数的逻辑是先判断传入的字符串里有没有BEGIN标记有就不处理没有的话去掉空白字符然后每 64 个字符一行拼上标准头尾。这里的 64 是约定俗成的换行宽度不换也行但加上更保险因为某些后端在解析带换行的 PEM 文件时对格式敏感。还有一个一定要确认的点公钥和私钥必须是一对。如果后端生成的私钥是 PKCS#1 格式公钥却是 PKCS#8两端封装方式不同但数学结构一致加解密也能通。真正导致失败的是密钥对本身不匹配或者公钥被传输过程截断。遇到setPublicKey后加密返回空先打印一下公钥字符串看看末尾有没有被截断。2.3 一次完整加解密闭环前端加密、用 Node 模拟后端解密只在前端能加密还不算数必须确认后端能解出来才算闭环。先看前端加密这一侧import JSEncrypt from jsencrypt const PUBLIC_KEY -----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUA...\n-----END PUBLIC KEY----- function rsaEncrypt(text) { const encryptor new JSEncrypt() encryptor.setPublicKey(PUBLIC_KEY) const result encryptor.encrypt(text) if (result false || result null) { // 常见情况是公钥格式错误或明文超长 throw new Error(RSA encrypt failed) } return result } const cipherText rsaEncrypt(hello rsa) console.log(cipherText) // 类似 eyJhbGciOi... 的 Base64 串这里的new JSEncrypt()每次加密都新建实例避免多个请求之间公钥状态互相污染。encrypt()返回false的时候代码主动抛错而不是把false传到后端这是在开发期暴露出问题的好习惯比线上拿到一个空串再去排查要快得多。再看 Node 后端怎么解密。用 Node 原生crypto模块就够了不需要额外引库const crypto require(crypto) const fs require(fs) const privateKey fs.readFileSync(private_key.pem, utf8) function rsaDecrypt(cipherBase64) { // Base64 字符串转成 Buffer const buffer Buffer.from(cipherBase64, base64) const decrypted crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, buffer) return decrypted.toString(utf8) } console.log(rsaDecrypt(cipherText)) // hello rsa注意padding参数写的是RSA_PKCS1_PADDING这是因为 jsencrypt 的encrypt()默认使用的就是 PKCS#1 v1.5 填充方式。如果后端用了 OAEP 填充解密会直接报错RSA_PKCS1_OAEP_PADDING不匹配。这个知识点是前后端联调最容易对不上的地方建议在接口文档里白纸黑字写明前端 jsencrypt 默认 PKCS#1后端解密也用 PKCS#1。3. 长文本分段加密超过 245 字节就翻车的处理方案3.1 为什么 RSA 有长度上限密钥位数、填充方式与可加密字节数RSA 不是一个“能加多少加多少”的加密算法。它一次能加密的明文长度受密钥位数和填充方式双重限制。以最常见的 2048 位密钥为例采用 PKCS#1 v1.5 填充时实际可加密的明文字节数为2048 / 8 - 11 245字节。这里减掉的 11 字节是填充开销。1024 位密钥则只能承载 117 字节。密钥位数最大明文字节PKCS#1 v1.5密文 Base64 长度1024117172 字符2048245344 字符3072373512 字符这个表直接回答了“为什么我加密短字符串没事一加密长文本就返回 false”。超过这个字节数jsencrypt 的encrypt()不会截断而是直接返回false。解决思路就是自己实现分段加密让每段都在安全字节数以内。3.2 按字节分段加密中文场景下不能直接 substr这里有一个很容易踩的细节中文一个字符占 3 个字节UTF-8。如果直接用 JavaScript 的substr截取 245 个字符那实际字节数可能是 700 多依然超限。所以分段必须按字节数来切而不是按字符数切。下面这个函数按 UTF-8 字节长度截取字符串function utf8Substr(text, maxBytes) { let bytes 0 let result for (let i 0; i text.length; i) { const charCode text.charCodeAt(i) // UTF-8 中 ASCII 占 1 字节中文占 3 字节 const byteLength charCode 127 ? 3 : 1 if (bytes byteLength maxBytes) { break } bytes byteLength result text[i] } return result }逻辑说明遍历字符串的每个字符逐个累加其字节数直到加上当前字符会超过maxBytes就停止。英文和数字按 1 字节算中文按 3 字节算这才是和 RSA 实际加密字节数对齐的判断方式。有了这个基础完整的分段加密函数就出来了import JSEncrypt from jsencrypt const PUBLIC_KEY -----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY----- // 2048 位密钥的安全段长 const MAX_BLOCK_SIZE 245 function encryptLong(text, publicKey) { const encryptor new JSEncrypt() encryptor.setPublicKey(publicKey) let offset 0 const segments [] while (offset text.length) { // 从当前位置截取一段不超过 245 字节的明文 const block utf8Substr(text.substring(offset), MAX_BLOCK_SIZE) const encryptedBlock encryptor.encrypt(block) if (encryptedBlock false) { throw new Error(RSA encrypt failed at offset ${offset}) } segments.push(encryptedBlock) offset block.length } // 用 | 连接每一段密文后端按相同分隔符拆开 return segments.join(|) }参数说明MAX_BLOCK_SIZE必须小于等于 245保守一点可以设成 200给填充留出足够余量。encryptor实例只创建了一次循环里反复调用同一个实例的encrypt()是安全的不会出现上下文互相覆盖的问题。最后用|拼接各段密文因为每段密文本身就是独立的 Base64 字符串|不会和它们冲突。3.3 分段解密按分隔符拆开逐段还原后端拿到|连接的长密文需要先拆开再逐段解密。解密侧同样可以封装const crypto require(crypto) function decryptLong(encryptedText, privateKey) { const segments encryptedText.split(|) const results [] for (const segment of segments) { if (segment ) continue const buffer Buffer.from(segment, base64) const decrypted crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, buffer) results.push(decrypted.toString(utf8)) } return results.join() }这段逻辑和前端的encryptLong完全对称前端用|拼起来后端用|拆开每段独立解密后再拼接。之所以不用整体Buffer.from后一次性解密是因为那会超过 RSA 单次解密的长度上限解密函数直接报错。这个分段方案是通用的前端用什么分隔符、每段多长后端只要拿到约定参数就能还原。我一般会在接口文档里加一句注释“密文按每段最长 245 字节明文切割段间以 | 分隔”。4. 避坑排查encrypt 返回 false 和 uniapp 解密失败的原因清单4.1 公钥头尾缺失导致 setPublicKey 静默失败现象前端调用encryptor.encrypt(abc)返回false控制台没有报错。原因setPublicKey解析不了没有BEGIN/END标记的裸 Base64 字符串或者公钥本身不是 PEM 格式。jsencrypt 对非法 key 的处理是静默的不会抛异常只会让后续encrypt()返回false。解决先用 2.2 节的formatPublicKey函数统一格式化再传入setPublicKey。调试时在encrypt外面包一层判断返回false就把公钥字符串JSON.stringify打印出来肉眼检查有没有被\n吃掉。4.2 公钥串粘贴进代码时混入空格和转义符现象从后端接口文档复制公钥到前端代码怎么试都返回false。仔细看字符串发现里面有不可见字符。原因很多后端返回的公钥自带\n字面量比如 Java 接口里返回的是-----BEGIN PUBLIC KEY-----\\nMIGf...\\n-----END PUBLIC KEY-----转成 JSON 后前端拿到的是带\\n的字符串不是真正的换行。另外复制粘贴时可能混入空格。解决在formatPublicKey里先用replace(/\\n/g, \n)处理转义再用replace(/\s/g, )去掉残留空白。这两个替换要按顺序执行先还原转义换行再压缩空格。4.3 明文超过 245 字节短文本正常长文本必挂现象加密test正常加密一段 JSON 或备注文本时返回false后端也收不到请求。原因明文超出密钥位数的单次加密上限2038 位密钥下 245 字节就是红线且这个限制对中文更不友好——245 字节只够装 81 个汉字。解决直接替换为第 3 章的分段方案。没有引入第三方库的必要自己封装encryptLong和decryptLong就够用。如果业务里传输内容经常超过 1KB考虑后端换对称加密方案前端用 RSA 保护 AES 密钥这是后话。4.4 uniapp 小程序端window is not defined报错现象uniapp 项目编译到微信小程序页面一加载就报window is not definedH5 端正常。原因jsencrypt 的JSEncrypt类内部引用了window和navigator这类浏览器全局对象小程序运行时没有这些对象。npm 打包时没有对这类浏览器依赖做垫片处理。解决方案有两种。其一在manifest.json的小程序配置里不做特殊处理直接改用条件编译在小程序端走后端接口完成加密。其二把jsencrypt.min.js下载后放到common/jsencrypt.js并在文件开头手动垫一个var window globalThis || {}。第二种方案实测可行但要注意globalThis在小程序基础库 2.2.3 以上的兼容性。4.5 后端解密乱码Base64 里的号被 URL 解码成空格现象前端加密后把密文拼进 URL 查询参数传给后端后端解出来前半段正常、后半段乱码或者解密时报ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH。原因Base64 字符集里包含、/和这些字符在 URL 传参时会被重新解析——会被当成空格密文被破坏。解决前端在把密文放进 URL 前先做encodeURIComponent后端先decodeURIComponent还原 Base64 再做解密。如果密文是放在 JSON body 里传输则没这个问题但排查顺序上要优先确认传输链路有没有对密文做过二次编码。5. uniapp 封装与本地自测把加解密做成一个模块再上线先做一个工具模块把密钥配置和加解密方法收拢在一起页面里只需要一行调用// utils/rsa.js import JSEncrypt from jsencrypt // 生产环境只保留公钥私钥绝不能出现在前端代码里 const PUBLIC_KEY -----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUA...\n-----END PUBLIC KEY----- const MAX_BLOCK_SIZE 245 function utf8Substr(text, maxBytes) { let bytes 0 let result for (let i 0; i text.length; i) { const byteLength text.charCodeAt(i) 127 ? 3 : 1 if (bytes byteLength maxBytes) break bytes byteLength result text[i] } return result } export function rsaEncrypt(text) { const encryptor new JSEncrypt() encryptor.setPublicKey(PUBLIC_KEY) const segments [] let offset 0 while (offset text.length) { const block utf8Substr(text.substring(offset), MAX_BLOCK_SIZE) const encryptedBlock encryptor.encrypt(block) if (encryptedBlock false) { throw new Error(RSA encrypt failed at offset ${offset}) } segments.push(encryptedBlock) offset block.length } return segments.join(|) }页面里调用时import { rsaEncrypt } from /utils/rsa.js const encryptedPassword rsaEncrypt(123456) // 提交给后端 loginApi({ username: admin, password: encryptedPassword })模块封装的意义在于密钥一旦更新只改utils/rsa.js一个文件所有页面同步生效不用去几十个业务文件里找硬编码的密钥串。上线前我习惯写一段自测逻辑用同一对测试密钥在控制台跑一次加密再解密// 仅开发环境使用验证密钥对和分段逻辑 import { rsaEncrypt } from /utils/rsa.js // 测试私钥只存在于本地用于闭环验证 import JSEncrypt from jsencrypt const TEST_PRIVATE_KEY -----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY----- const cipher rsaEncrypt(测试中文字符串的长度边界) const decryptor new JSEncrypt() decryptor.setPrivateKey(TEST_PRIVATE_KEY) console.log(decryptor.decrypt(cipher))这段自测代码能一次性验证三件事密钥对是否匹配、分段切分是否精准、中文 UTF-8 截断是否在边界内。我自己的习惯是任何一次密钥更新或环境更换都必须先跑通这个闭环再往下走。曾经有次上线前偷懒没跑自测结果接入新需求时公钥换成 PKCS#8 的登录接口全部返 500。从那以后我再也没有跳过这套验证流程哪怕只是改一个字符也要重跑一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表