手把手实现前后端RSA加密通信:Python与JavaScript实战指南 1. 项目概述为什么需要手搓RSA加密通信在前后端分离的现代Web开发里数据安全是个绕不开的话题。你肯定遇到过这样的场景用户在前端页面输入了密码、身份证号或者支付信息点击提交这些敏感数据就通过网络飞向了后端服务器。这个传输过程如果用的是最基础的HTTP协议数据就是“裸奔”的任何一个中间环节都可能被截获和窥探。虽然HTTPSHTTP over SSL/TLS已经成为标配它能解决传输层的加密问题但有时候我们还需要在应用层再加一把锁。比如后端需要对前端提交的密文进行签名验证或者前端需要加密一些在本地存储的配置信息防止被轻易篡改。这时候非对称加密算法RSA就派上用场了。RSA这个名字你可能不陌生它是由三位科学家姓氏首字母组成的。它的核心魅力在于“非对称”有一对密钥一个叫公钥Public Key可以大大方方发给任何人另一个叫私钥Private Key必须自己严加保管。用公钥加密的数据只有对应的私钥才能解开反过来用私钥签名的数据任何人都能用公钥验证其真伪。这个特性天生就适合前后端通信后端生成密钥对把公钥丢给前端前端用这个公钥加密敏感数据再发送后端收到后用自己的私钥解密。这样一来即使请求被截获攻击者没有私钥也束手无策。这个项目就是带你从零开始用Python作为后端用原生JavaScript作为前端实现一套完整的RSA加密解密通信流程。我们不依赖那些高度封装的、一键完成所有步骤的第三方库虽然它们很好用而是选择相对底层的cryptographyPython和jsencryptJS库目的是让你能看清密钥的格式、加密的输入输出、以及数据在前后端流转时需要的那些关键转换步骤。相信我亲手实现一遍之后你对证书、密钥、编码这些概念的理解会深刻得多。2. 核心原理与工具选型为什么是它们在动手之前我们得先搞清楚要用什么工具以及为什么选它们。市面上相关的库很多每个选择背后都有权衡。2.1 RSA算法再认识不是所有RSA都一样首先得破除一个误区不是说用了RSA就万事大吉了。RSA算法本身在实现时有很多参数和模式选错了轻则无法解密重则存在安全风险。密钥长度这是安全性的基石。现在普遍认为1024位的RSA密钥已经不够安全容易被暴力破解。所以我们的项目直接从2048位起步。cryptography库默认生成的就是2048位这是一个当前兼顾性能与安全性的选择。当然对安全性要求极高的场景如金融根证书可以考虑4096位但加解密速度会明显下降。填充方案Padding这是最容易出问题的地方。原始的RSA算法是“教科书式”的存在固有的安全缺陷。因此在实际使用时必须对明文进行填充。最常见的两种是PKCS1_v1_5 比较老的填充标准仍然被广泛支持但在某些特定情况下可能存在风险。OAEPOptimal Asymmetric Encryption Padding 目前推荐使用的填充方式安全性比PKCS1_v1_5更高。我们的项目将统一使用OAEP填充。编码与格式密钥和加密后的数据在计算机里都是一串二进制字节bytes。为了在网络传输或存储中方便处理我们需要把它们编码成文本。最常用的就是Base64和PEM格式。PEM格式 这是一种用来封装密钥和证书的文本格式通常以-----BEGIN XXX-----和-----END XXX-----包裹着Base64编码的内容。它非常常见易于阅读和分发。Base64字符串 纯粹的数据编码没有PEM的头尾标记更紧凑。前端jsencrypt库通常需要PEM格式的公钥。2.2 后端工具Python cryptography为什么选cryptography而不是rsa或PyCryptocryptography 这是目前Python生态中密码学库的“事实标准”。它底层由C语言实现速度快且由专业的密码学工程师维护API设计现代、清晰安全性有保障。它同时支持高层次易于使用和低层次高度控制的API。rsa 一个纯Python实现的库虽然简单易懂但性能较差且在一些高级特性和安全性更新上可能滞后。PyCrypto/PyCryptodome 功能强大但API较为老旧和复杂对于新手不够友好。我们的项目将使用cryptography.hazmat.primitives.asymmetric模块下的rsa和padding来生成密钥、进行加解密。hazmat是“危险材料”的意思提醒我们这里提供的都是底层原语需要正确使用。2.3 前端工具JSEncrypt为什么选jsencrypt而不是node-rsa或自己用Web Crypto APIjsencrypt 一个专门为浏览器环境设计的、轻量级的RSA加密库。它最大的优点是API极其简单几行代码就能完成加密并且完美支持从PEM格式字符串加载公钥。这对于前端开发者来说非常友好。Web Crypto API 这是浏览器原生的加密API更安全、性能可能更好。但是它的API相对复杂并且对密钥格式尤其是导入PEM格式的密钥支持不那么直接需要额外的转换步骤对新手门槛较高。node-rsa 主要用于Node.js后端环境在纯浏览器端使用不便。jsencrypt封装了这些复杂性让我们能专注于业务逻辑。它内部默认使用的也是OAEP填充与我们的后端设置匹配。注意jsencrypt是一个纯前端库它的私钥操作如解密、签名在浏览器中执行是不安全的因为私钥可能会泄露。在我们的场景中前端只负责用公钥加密私钥解密永远在后端安全的服务器环境中进行。3. 后端实现生成密钥与提供接口后端的任务是三件事生成RSA密钥对、提供一个接口让前端获取公钥、提供另一个接口接收前端加密数据并解密。3.1 环境准备与依赖安装首先确保你的Python环境建议3.7以上已经就绪。我们使用pip安装必需的库。除了cryptography我们还需要一个Web框架来提供接口这里选择轻量级的Flask。pip install cryptography flask如果安装速度慢可以考虑使用国内的镜像源例如pip install cryptography flask -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 核心代码分步解析我们创建一个名为rsa_server.py的文件。第一步生成并持久化RSA密钥对密钥对生成一次即可不需要每次启动服务都重新生成。我们将它们保存为PEM格式的文件。from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization import os def generate_rsa_key_pair(): 生成2048位的RSA私钥和对应的公钥并保存为PEM文件。 # 生成私钥 private_key rsa.generate_private_key( public_exponent65537, # 标准公钥指数固定用这个值 key_size2048, ) # 序列化私钥为PEM格式不加密 private_pem private_key.private_bytes( encodingserialization.Encoding.PEM, formatserialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithmserialization.NoEncryption() # 生产环境应考虑加密存储私钥 ) # 从私钥导出公钥 public_key private_key.public_key() public_pem public_key.public_bytes( encodingserialization.Encoding.PEM, formatserialization.PublicFormat.SubjectPublicKeyInfo ) # 保存到文件 with open(private_key.pem, wb) as f: f.write(private_pem) with open(public_key.pem, wb) as f: f.write(public_pem) print(RSA密钥对已生成并保存为 private_key.pem 和 public_key.pem) return private_key, public_key # 如果文件不存在则生成密钥 if not os.path.exists(private_key.pem): generate_rsa_key_pair()关键点解析public_exponent65537 这是一个质数二进制表示为10000000000000001在安全性和计算效率之间取得了很好的平衡是RSA的标准选择。serialization.NoEncryption() 这里为了演示简便私钥文件没有用密码进行二次加密。在生产环境中这是极其危险的私钥文件必须用强密码加密存储并在程序运行时从安全的地方如环境变量、密钥管理服务获取密码后再加载。生成的public_pem正是前端jsencrypt所需要的格式。第二步创建Flask应用与接口from flask import Flask, jsonify, request from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes import base64 app Flask(__name__) # 从文件加载密钥启动时加载一次 with open(private_key.pem, rb) as f: private_key serialization.load_pem_private_key( f.read(), passwordNone, # 如果私钥有密码这里需要提供 ) with open(public_key.pem, rb) as f: public_key_pem f.read().decode(utf-8) # 转换为字符串供前端使用 app.route(/get_public_key, methods[GET]) def get_public_key(): 接口1向前端返回公钥PEM格式字符串 return jsonify({public_key: public_key_pem}) app.route(/decrypt_data, methods[POST]) def decrypt_data(): 接口2接收前端加密的Base64数据并用私钥解密 data request.get_json() if not data or encrypted_data not in data: return jsonify({error: Missing encrypted_data}), 400 encrypted_b64 data[encrypted_data] try: # 1. 前端传回的是Base64字符串需要解码为字节 encrypted_bytes base64.b64decode(encrypted_b64) # 2. 使用私钥进行解密填充方式必须与前端加密时一致OAEP decrypted_bytes private_key.decrypt( encrypted_bytes, padding.OAEP( mgfpadding.MGF1(algorithmhashes.SHA256()), algorithmhashes.SHA256(), labelNone ) ) # 3. 将解密后的字节转换为字符串假设前端加密的是文本 decrypted_text decrypted_bytes.decode(utf-8) return jsonify({ status: success, decrypted_data: decrypted_text }) except Exception as e: # 捕获所有异常例如填充错误、数据被篡改、密钥不匹配等 return jsonify({error: fDecryption failed: {str(e)}}), 400 if __name__ __main__: app.run(debugTrue, port5000)关键点解析padding.OAEP 这里明确指定了OAEP填充方案并使用了SHA256作为哈希算法。这个配置必须与前端的jsencrypt默认配置完全匹配否则解密会失败。jsencrypt默认使用的就是SHA256的OAEP。base64.b64decode 这是一个关键转换。网络传输中二进制数据不方便所以前端会将加密后的字节数组进行Base64编码再传输。后端必须先解码才能进行解密操作。错误处理 解密过程可能因为多种原因失败数据损坏、密钥不对、填充错误。用try...except捕获异常并返回友好的错误信息至关重要避免将服务器内部细节暴露给客户端。4. 前端实现加密与发送数据前端的工作流程是页面加载后从后端获取公钥当用户提交表单时用公钥加密敏感字段将加密后的Base64字符串通过AJAX发送给后端解密接口。4.1 引入JSEncrypt库你可以直接使用CDN或者下载jsencrypt.min.js到本地。这里使用CDN方式在HTML的head中引入。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleRSA加密通信演示/title script srchttps://cdn.jsdelivr.net/npm/jsencrypt3.3.1/bin/jsencrypt.min.js/script /head body !-- 页面内容 -- /body /html4.2 核心JavaScript逻辑我们在页面中添加一个简单的表单并编写对应的JavaScript代码。body h2敏感信息提交RSA加密/h2 form idsecureForm div label forusername用户名/label input typetext idusername nameusername /div div label forpassword密码/label input typepassword idpassword namepassword /div button typesubmit提交/button /form div idresult/div script // 全局变量用于存储从后端获取的公钥 let publicKeyPem ; // 页面加载完成后自动从后端获取公钥 window.onload function() { fetch(http://localhost:5000/get_public_key) .then(response response.json()) .then(data { publicKeyPem data.public_key; console.log(公钥获取成功); document.getElementById(result).innerHTML p stylecolor:green;已获取服务器公钥可以安全提交。/p; }) .catch(error { console.error(获取公钥失败:, error); document.getElementById(result).innerHTML p stylecolor:red;无法获取公钥请检查后端服务。/p; }); }; // 表单提交事件处理 document.getElementById(secureForm).addEventListener(submit, function(event) { event.preventDefault(); // 阻止表单默认提交 if (!publicKeyPem) { alert(公钥未就绪请稍后再试。); return; } const username document.getElementById(username).value; const password document.getElementById(password).value; // 构造要加密的数据对象通常只加密敏感字段 const plainData { password: password // 只加密密码用户名可以明文传输 // 你可以根据需要添加其他字段如身份证号、手机号等 }; const plainText JSON.stringify(plainData); // 转换为JSON字符串 // 使用JSEncrypt进行加密 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKeyPem); // 设置公钥 const encryptedBase64 encryptor.encrypt(plainText); // 加密并返回Base64字符串 if (!encryptedBase64) { alert(加密失败请检查公钥格式或待加密数据。); return; } console.log(加密后的Base64数据:, encryptedBase64); // 将加密后的数据发送到后端解密接口 fetch(http://localhost:5000/decrypt_data, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ username: username, // 明文用户名 encrypted_data: encryptedBase64 // 加密后的密码数据 }) }) .then(response response.json()) .then(data { const resultDiv document.getElementById(result); if (data.status success) { // 解密成功显示解密后的数据仅用于演示生产环境不应显示密码 const decryptedObj JSON.parse(data.decrypted_data); resultDiv.innerHTML p stylecolor:green;提交成功/p p后端解密出的密码是${decryptedObj.password}/p p此为演示实际场景不会返回密码明文/p; console.log(后端解密结果:, decryptedObj); } else { resultDiv.innerHTML p stylecolor:red;提交失败${data.error}/p; } }) .catch(error { console.error(请求失败:, error); document.getElementById(result).innerHTML p stylecolor:red;网络请求异常${error.message}/p; }); }); /script /body关键点解析encryptor.setPublicKey(publicKeyPem) 这是最关键的一步必须确保传入的publicKeyPem是完整的、格式正确的PEM字符串包含-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----。encryptor.encrypt(plainText) 该方法接受一个字符串参数返回一个Base64编码的字符串。它内部完成了RSA-OAEP加密和Base64编码。加密策略 我们只加密了password字段而username是明文传输的。这是一种常见的权衡因为用户名通常不算是最高机密且可能需要用于日志或查询。你可以根据实际需求决定加密哪些字段。注意RSA算法本身有长度限制与密钥长度有关加密的数据不能太大。对于很长的数据通常采用“混合加密”用RSA加密一个随机的对称密钥如AES密钥再用这个对称密钥加密大量数据。跨域问题 本例中前端页面通过file://协议打开直接请求localhost:5000会遇到跨域问题CORS。为了解决这个问题你需要在后端Flask应用中添加CORS支持或者使用一个简单的HTTP服务器如python -m http.server来运行前端页面使其通过http://协议访问。更简单的方法是在后端app.run()之前添加一个Flask的CORS扩展或者手动添加响应头仅用于开发测试app.after_request def add_cors_headers(response): response.headers[Access-Control-Allow-Origin] * response.headers[Access-Control-Allow-Headers] Content-Type return response注意 生产环境中应将*替换为具体的前端域名并配置更严格的CORS策略。5. 运行、测试与问题排查5.1 完整运行流程准备后端将上面的rsa_server.py代码保存。在终端进入文件所在目录运行python rsa_server.py。首次运行会生成private_key.pem和public_key.pem文件。看到输出* Running on http://127.0.0.1:5000/表示后端启动成功。准备前端将上面的HTML和JS代码保存为index.html。由于有跨域请求不能直接双击打开。可以在index.html所在目录打开终端运行一个简单的HTTP服务器# Python 3 python -m http.server 8000然后在浏览器中访问http://localhost:8000。测试打开浏览器控制台F12刷新页面应该能看到“公钥获取成功”的日志。在表单中输入用户名和密码点击提交。观察浏览器控制台的网络请求可以看到一个POST /decrypt_data的请求请求体里包含了encrypted_data这个长长的Base64字符串。如果一切正常页面上会显示“提交成功”以及后端解密出的密码仅演示。后端终端也会打印出相应的请求日志。5.2 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这几个问题。我把它们和解决方法整理成了表格。问题现象可能原因排查步骤与解决方案前端报错JSEncrypt is not defined1.jsencrypt库未成功加载。2. 脚本执行顺序问题。1. 检查浏览器开发者工具的“网络(Network)”标签页确认jsencrypt.min.js是否加载成功状态码200。2. 确保script标签在调用JSEncrypt的代码之前。前端加密失败encrypt()返回false1. 公钥字符串格式错误或损坏。2. 待加密数据不是字符串类型。3. 待加密数据过长超出RSA密钥长度限制。1. 在setPublicKey后打印encryptor.getPublicKey()或直接console.log(publicKeyPem)检查公钥字符串是否完整首尾的-----BEGIN...标记是否齐全中间是否有非法字符或换行符丢失。2. 确保plainText是字符串。使用JSON.stringify转换对象。3. 对于2048位密钥OAEP填充下能加密的明文长度大约在190字节左右。如果数据太长需要采用“混合加密”方案。后端解密失败报填充错误 (Invalid padding)这是最常见的问题前后端填充方案不匹配。1.确认后端使用的是padding.OAEP并且哈希算法是hashes.SHA256()。2.确认前端jsencrypt是较新版本如3.x。旧版本可能默认使用PKCS1_v1_5。你可以尝试在前端初始化时指定const encryptor new JSEncrypt({default_key_size: 2048, default_public_exponent: 010001, log: false});但主要确保库版本。后端解密失败报ValueError: Encryption/decryption failed1. 前端传过来的Base64字符串在传输过程中可能被修改如URL编码问题。2. 私钥与加密用的公钥不匹配。1. 在后端解密前打印接收到的encrypted_b64与前端加密后打印的encryptedBase64进行对比看是否一致。注意Base64字符串中的、/、在URL传输时可能需要特殊处理。如果通过URL参数传递需使用encodeURIComponent和decodeURIComponent。我们用的是POST JSON通常没问题。2. 确保后端加载的private_key.pem与当初生成public_key.pem的私钥是同一对。可以重新生成一对密钥试试。浏览器控制台报CORS跨域错误前端页面源如http://localhost:8000与后端API源http://localhost:5000不同。按照上文【关键点解析4】的方法在后端Flask应用中添加CORS响应头。这是开发阶段最便捷的解决方案。生产环境需精确配置。前端获取公钥失败后端/get_public_key接口未启动或路径错误。1. 直接在浏览器访问http://localhost:5000/get_public_key看是否能返回JSON数据。2. 检查前端fetch的URL是否正确。3. 检查后端Flask服务是否正常运行有无报错。一个关键的实操心得当遇到解密失败时不要只看后端错误日志。一定要打开浏览器的开发者工具在“网络(Network)”标签页中找到那个发送加密数据的请求点击查看“请求负载(Request Payload)”把里面的encrypted_data值完整复制出来。然后写一个简单的Python测试脚本用你的私钥手动解密这个字符串看看是否能成功。这样可以快速定位问题是出在前端加密环节还是后端解密环节或者是数据传输环节。6. 进阶考量与安全实践实现基础功能只是第一步要把这套机制用于真实项目还需要考虑更多。6.1 性能与数据长度限制RSA算法计算比较慢且加密的数据长度受密钥长度限制。2048位密钥的RSA其能加密的明文长度字节约为密钥长度/8 - 填充开销。对于OAEP填充这个值大约在190字节左右。这意味着你不能用它直接加密一整篇文章或一张图片。正确的做法是采用“混合加密”前端随机生成一个对称密钥比如AES-256的密钥。前端用这个对称密钥加密你的大量数据如文件、长文本。前端用后端的RSA公钥加密这个对称密钥。前端将加密后的对称密钥和用该对称密钥加密的数据一起发送给后端。后端用RSA私钥解密出对称密钥再用对称密钥解密出原始数据。这样既利用了RSA的非对称特性安全交换密钥又利用了对称加密算法的高效性来处理大数据。6.2 密钥管理与轮转私钥安全 再次强调private_key.pem绝不能放在代码仓库或能被公开访问的地方。生产环境中应该使用密码加密存储私钥文件。将密码存储在环境变量或专业的密钥/密码管理服务如HashiCorp Vault, AWS KMS中。在应用启动时从安全源加载密钥。公钥分发 我们的例子是通过一个接口动态获取。也可以考虑将公钥直接嵌入前端代码或配置文件但这样不利于轮转。密钥轮转 任何密钥都不应该永久使用。应制定策略定期如每年更换密钥对。更换时需要有一个过渡期新旧公钥同时有效以确保正在进行的会话或请求不会中断。6.3 完善通信安全我们的示例只实现了“加密”一个完整的安全通信还需要考虑防重放攻击 攻击者可能截获你加密的请求数据包然后原封不动地重复发送给服务器。可以在待加密的数据中加入一个时间戳和随机数Nonce后端验证该时间戳在合理窗口内且随机数未被使用过。完整性校验 虽然RSA-OAEP本身提供一定的完整性保护但更常见的做法是同时进行签名。后端可以用私钥对响应数据签名前端用公钥验签确保数据在传输过程中未被篡改。HTTPS是基础应用层RSA加密绝不能替代HTTPSHTTPS提供了端到端的传输层加密、服务器身份认证防中间人攻击和完整性保护。我们实现的RSA加密是在HTTPS这个安全通道之上对最敏感的数据再加的一把锁属于“纵深防御”策略。6.4 前端代码的隐蔽性我们前端的公钥是硬编码或从接口获取的这本身是公开信息没问题。但加密逻辑是暴露在浏览器源码中的。虽然混淆和压缩能增加一点分析难度但一个有心的攻击者仍然可以分析出你的加密流程。因此前端安全永远是一种“增加攻击成本”的博弈核心机密和关键逻辑必须放在后端。不要试图在前端隐藏真正的加密算法或密钥。手把手实现一遍之后你会发现RSA加密通信的脉络变得非常清晰生成密钥、分发公钥、前端加密、后端解密。每一步的坑比如填充方案要对齐、Base64编码解码、CORS问题、数据长度限制都是实践中必须跨过去的坎。这套流程不仅适用于密码传输任何需要前端保密提交的数据比如问卷中的个人信息、一键登录的临时令牌等都可以套用这个模式。

相关新闻

最新新闻

RsaCtfTool终极指南:快速掌握RSA攻击与密码学实战技巧

RsaCtfTool终极指南:快速掌握RSA攻击与密码学实战技巧

RsaCtfTool终极指南:快速掌握RSA攻击与密码学实战技巧 【免费下载链接】RsaCtfTool RSA attack tool (mainly for ctf) - retrieve private key from weak public key and/or uncipher data 项目地址: https://gitcode.com/gh_mirrors/rs/RsaCtfTool 想要在C…

2026/7/3 7:07:50
HCIP作业8

HCIP作业8

2026/7/3 7:07:50
【03】Function Calling:让 LLM 拥有双手

【03】Function Calling:让 LLM 拥有双手

Function Calling:让 LLM 拥有双手 基于 Lion-1209/AgentStudy 仓库,对应代码见 stage1-fundamentals/task1.2_tool_use.py 和 API_REFERENCE.md 为什么需要 Function Calling? 上一篇我们用正则解析 LLM 的文本输出来获取工具调用信息。这种…

2026/7/3 7:07:50
2026 商业秘密新规|代码、算法、研发图纸作为技术秘密的三大法定认定条件

2026 商业秘密新规|代码、算法、研发图纸作为技术秘密的三大法定认定条件

对于软件企业、智能制造、硬件研发公司,核心竞争力集中在源代码、自研算法、工艺图纸、试验仿真数据等技术类信息。2026 年 6 月实施《商业秘密保护规定》明确将算法、电子程序纳入技术秘密保护范围,但大量研发企业遭遇员工带走代码、竞品窃取工艺后维权…

2026/7/3 7:07:50
Claude Code / Agent 术语说明文档

Claude Code / Agent 术语说明文档

一、整体理解在 Claude Code 里,可以把系统理解成一个“带工具的程序员代理”:LLM 负责理解和推理,Agent 负责组织任务,Tools / Function Calling / MCP 负责执行真实操作,Context Window / Memory / RAG 负责提供和保…

2026/7/3 7:07:49
构建能理解if-else的聊天机器人:条件语句解析与规则引擎实践

构建能理解if-else的聊天机器人:条件语句解析与规则引擎实践

1. 项目概述:让聊天机器人真正“听懂”if-else背后的逻辑你有没有试过跟某个客服机器人说:“如果订单还没发货,就帮我取消;否则,请把物流单号发给我。”结果它要么只执行了前半句,要么直接回复“抱歉&#…

2026/7/3 7:02:49

周新闻

月新闻