前端SM2国密算法实战:从sm-crypto封装到前后端联调指南

发布时间:2026/6/30 3:39:54

前端SM2国密算法实战:从sm-crypto封装到前后端联调指南 1. 项目概述为什么前端需要关注SM2国密最近在做一个和政府、金融相关的项目对接方明确要求所有敏感数据传输必须使用国密算法特别是非对称加密部分指定了SM2。说实话一开始我也懵了一下毕竟平时RSA用惯了前端加解密库一抓一大把。但真到了项目里才发现SM2在前端的生态和RSA完全不是一个量级从库的选择、安装到实际封装调用每一步都可能有坑。网上资料要么是零散的片段要么是后端视角的Java实现真正能跑通的前端示例少之又少。所以我把自己从零搭建、封装到最终上线的完整过程连同踩过的那些坑都整理出来。如果你也面临类似的需求或者单纯想在前端技术栈里加入国密能力这篇实战指南应该能帮你省下不少折腾的时间。简单来说SM2是一种基于椭圆曲线密码学的非对称加密算法属于国家密码管理局发布的商用密码标准体系。和RSA相比在相同安全强度下SM2的密钥长度更短256位对标RSA 2048位计算速度更快且被认为能抵御量子计算机的某些攻击。在前端场景下它的核心应用就是数据加密传输和数字签名验签。比如用户登录时用后端下发的公钥加密密码或者对提交的表单数据进行签名确保数据的机密性、完整性和不可否认性。2. 核心思路与方案选型为什么是sm-crypto确定了要用SM2接下来就是选型。前端实现加密无非几种路径纯JavaScript实现、WebAssemblyWASM封装、或者寻找现成的NPM包。经过一番调研和试错我最终选择了sm-crypto这个库理由如下2.1 主流方案对比forge/jsrsasign 自定义SM2实现这两个是前端老牌的加密库支持RSA、AES等但对国密算法的原生支持非常弱。你需要自己去找SM2的椭圆曲线参数、实现点乘、哈希等底层逻辑工作量巨大且极易出错安全性和性能都无法保证。对于业务开发来说这基本是不可行的路线。gm-crypt这也是一个比较知名的国密算法库。但它更偏向于一个“全家桶”同时支持SM2、SM3、SM4。在早期测试中我发现其SM2部分在某些边界情况如加密超长文本下的表现不太稳定且文档相对简略。社区活跃度和issue的响应速度略逊于sm-crypto。sm-crypto这个库由 JuneAndGreen 开发专注于SM2和SM3。它的优势非常明显API设计简洁直观与Node.js的crypto模块风格类似学习成本低纯JavaScript实现无需额外编译或WASM支持兼容性极好文档清晰提供了丰富的示例最重要的是经过大量线上项目验证社区活跃遇到的问题通常能找到解决方案。虽然纯JS在极端性能场景下可能不如WASM但对于绝大多数前端加密场景单次加密/解密数据量在KB级别其性能完全足够。2.2 决策背后的考量选择sm-crypto核心是权衡了“开发效率”、“稳定性”和“社区支持”。开发效率它提供了encrypt、decrypt、doSignature、doVerifySignature等开箱即用的方法我们不需要关心椭圆曲线数学。稳定性作为专门实现其算法正确性更有保障。我们的项目对接银行系统加解密失败会导致整个流程阻塞稳定性是第一位。社区支持GitHub上stars数量、近期的commit记录以及打开的issue能否被及时响应是评估一个开源库能否用于生产环境的重要指标。sm-crypto在这方面表现良好。注意sm-crypto目前主要提供SM2和SM3哈希算法。如果你的项目还需要SM4对称加密可能需要额外引入如gm-crypt的SM4部分或者寻找其他专门的SM4库。本文聚焦SM2。3. 环境准备与安装避开版本与构建的“天坑”选好了库安装应该是小事一桩但就在这里我遇到了第一个大坑直接导致开发服务器启动失败。3.1 基础安装与版本锁定npm install sm-crypto --save # 或 yarn add sm-crypto看起来很简单。但请注意强烈建议你锁定一个具体版本而不是使用^或~这样的范围版本。国密算法库相对小众不同版本间可能存在不兼容的API变更。我最初使用了^0.3.0结果在某个次版本更新后加密结果突然和后端对不上了排查了半天才发现是库内部哈希处理逻辑有细微调整。教训惨痛。推荐使用固定版本// package.json dependencies: { sm-crypto: 0.3.5 // 以当前稳定版本为例请查阅最新版本 }3.2 构建工具适配Webpack/Vite核心避坑点这是整个安装环节最容易出问题的地方。sm-crypto的入口文件在其package.json中可能这样指定{ main: index.js }而它的index.js内部可能会根据环境判断使用crypto模块Node.js原生模块。在浏览器环境中这个模块是不存在的。现代前端构建工具如Webpack 5 和 Vite默认不再自动注入Node.js核心模块的polyfill。问题现象项目运行或构建时控制台报错Module not found: Error: Cant resolve crypto或global is not defined。解决方案对于 Webpack 5 项目安装必要的polyfill包npm install crypto-browserify stream-browserify buffer --save-dev在webpack.config.js中配置resolve.fallback// webpack.config.js module.exports { // ... 其他配置 resolve: { fallback: { crypto: require.resolve(crypto-browserify), stream: require.resolve(stream-browserify), buffer: require.resolve(buffer/) } } };如果还有global未定义错误需要在入口文件如src/index.js或src/main.js最顶部添加import { Buffer } from buffer; import process from process; window.global window; // 或 global globalThis; window.Buffer Buffer; window.process process;或者安装node-polyfill-webpack-plugin插件并配置。对于 Vite 项目 Vite 的处理更简单一些通常不需要修改配置。如果遇到crypto错误可以尝试安装crypto-browserify和stream-browserify。在vite.config.js中通过optimizeDeps或resolve.alias进行替换但通常不需要。更常见的做法是确保sm-crypto被正确预构建。如果问题依旧可以显式配置// vite.config.js import { defineConfig } from vite export default defineConfig({ optimizeDeps: { include: [sm-crypto] }, define: { global: globalThis, // 处理global未定义 } })3.3 验证安装创建一个简单的测试文件验证库是否能正常导入和使用// test-sm2.js import { sm2 } from sm-crypto; const keypair sm2.generateKeyPairHex(); console.log(公钥:, keypair.publicKey); console.log(私钥:, keypair.privateKey); console.log(SM2库导入成功);在浏览器控制台或Node环境中运行能正常输出密钥对即表示安装成功。4. SM2核心API详解与封装实践库装好了接下来就是怎么用它。sm-crypto的SM2 API主要围绕几个核心方法理解它们的输入输出是正确封装的关键。4.1 密钥生成SM2的密钥对包括一个公钥publicKey和一个私钥privateKey。公钥用于加密和验证签名私钥用于解密和生成签名。sm-crypto提供了便捷的生成方法。import { sm2 } from sm-crypto; // 生成密钥对十六进制字符串格式 const keypair sm2.generateKeyPairHex(); const publicKey keypair.publicKey; // 04 开头的130位十六进制串含04 const privateKey keypair.privateKey; // 64位十六进制串 console.log(公钥:, publicKey); console.log(私钥:, privateKey);重要提示生成的公钥通常是04开头后面跟着X和Y坐标各64位十六进制数总长130位。这个04代表未压缩格式是SM2标准中常见的形式。后端系统如Java的BouncyCastle库通常也期望接收这种格式的公钥。私钥则是64位十六进制数。4.2 数据加密与解密这是最常用的功能。前端用后端提供的公钥加密数据将密文传给后端后端用对应的私钥解密。/** * 使用公钥加密文本 * param {string} plainText - 待加密的明文 * param {string} publicKey - 公钥十六进制字符串通常以04开头 * param {string} [cipherMode0] - 加密模式0C1C2C3旧标准1C1C3C2新标准/更常见。需与后端协商一致 * returns {string} 加密后的密文十六进制字符串 */ function encryptData(plainText, publicKey, cipherMode 1) { // sm2.doEncrypt 要求明文是utf8字符串输出是十六进制密文 const cipherText sm2.doEncrypt(plainText, publicKey, cipherMode); return cipherText; } /** * 使用私钥解密文本通常在前端用于解密后端返回的加密数据但前端一般不持有私钥 * param {string} cipherText - 密文十六进制字符串 * param {string} privateKey - 私钥十六进制字符串 * param {string} [cipherMode1] - 解密模式必须与加密时一致 * returns {string} 解密后的明文 */ function decryptData(cipherText, privateKey, cipherMode 1) { // sm2.doDecrypt 要求密文是十六进制字符串输出是utf8明文 const plainText sm2.doDecrypt(cipherText, privateKey, cipherMode); return plainText; } // 使用示例 const mySecret Hello, SM2!; const encrypted encryptData(mySecret, publicKey, 1); console.log(加密结果:, encrypted); const decrypted decryptData(encrypted, privateKey, 1); console.log(解密结果:, decrypted); // 应输出 Hello, SM2!4.3 数字签名与验签签名用于验证数据的完整性和来源。发送方用私钥对数据或数据的哈希值签名接收方用公钥验证签名。/** * 使用私钥对消息进行签名 * param {string} msg - 原始消息字符串 * param {string} privateKey - 私钥 * param {object} [options] - 可选参数如 {hash: true, publicKey}hashtrue表示先对msg做SM3哈希再签 * returns {string} 签名结果十六进制字符串 */ function signMessage(msg, privateKey, options { hash: true, publicKey: }) { // 推荐使用 hash: true即对消息先进行SM3哈希再对哈希值签名。publicKey参数在某些特定计算中需要。 const sig sm2.doSignature(msg, privateKey, options); return sig; } /** * 使用公钥验证签名 * param {string} msg - 原始消息字符串 * param {string} sig - 签名十六进制字符串 * param {string} publicKey - 公钥 * param {object} [options] - 必须与签名时的options一致 * returns {boolean} 验证是否通过 */ function verifySignature(msg, sig, publicKey, options { hash: true, publicKey: }) { const isValid sm2.doVerifySignature(msg, sig, publicKey, options); return isValid; } // 使用示例 const message 这是一笔重要交易金额100元; const signature signMessage(message, privateKey, { hash: true }); console.log(签名:, signature); const isVerified verifySignature(message, signature, publicKey, { hash: true }); console.log(验签结果:, isVerified); // 应为 true5. 生产级封装构建健壮的加密工具类直接调用库的API虽然可以工作但在生产项目中我们需要一个更健壮、易用、可维护的封装。下面分享我封装的一个SM2Crypto工具类它处理了错误、模式配置、密钥管理和常见场景。5.1 工具类完整代码// utils/sm2Crypto.js import { sm2 } from sm-crypto; /** * SM2国密加密工具类 * 默认使用C1C3C2模式cipherMode1与大多数Java后端兼容。 * 使用前请务必与后端确认加密模式、公钥格式等细节。 */ class SM2Crypto { /** * param {string} publicKey - 加密公钥前端持有用于加密 * param {string} privateKey - 解密私钥前端通常不持有仅用于解密后端返回的数据或本地签名 * param {number} cipherMode - 加密/解密模式默认1 (C1C3C2) */ constructor(publicKey , privateKey , cipherMode 1) { this.publicKey publicKey; this.privateKey privateKey; this.cipherMode cipherMode; this._validateKeys(); } // 简单的密钥格式校验非常基础实际应根据后端要求加强 _validateKeys() { if (this.publicKey !/^04[0-9a-fA-F]{128}$/.test(this.publicKey)) { console.warn(公钥格式可能不正确预期为04开头的130位十六进制数。); } if (this.privateKey !/^[0-9a-fA-F]{64}$/.test(this.privateKey)) { console.warn(私钥格式可能不正确预期为64位十六进制数。); } } /** * 设置/更新公钥 */ setPublicKey(publicKey) { this.publicKey publicKey; this._validateKeys(); } /** * 设置/更新私钥谨慎使用 */ setPrivateKey(privateKey) { this.privateKey privateKey; this._validateKeys(); } /** * 加密字符串 * param {string} plainText - 明文 * param {string} publicKey - 可选若不传则使用实例公钥 * returns {string} 十六进制密文 * throws {Error} 当公钥未设置或加密失败时抛出 */ encrypt(plainText, publicKey this.publicKey) { if (!publicKey) { throw new Error(加密公钥未设置); } if (typeof plainText ! string) { // 如果是对象可以序列化。但更推荐调用方自己处理。 plainText JSON.stringify(plainText); } try { return sm2.doEncrypt(plainText, publicKey, this.cipherMode); } catch (error) { console.error(SM2加密失败:, error); throw new Error(加密失败: ${error.message}); } } /** * 解密字符串 * param {string} cipherTextHex - 十六进制密文 * param {string} privateKey - 可选若不传则使用实例私钥 * returns {string} 明文 * throws {Error} 当私钥未设置或解密失败时抛出 */ decrypt(cipherTextHex, privateKey this.privateKey) { if (!privateKey) { throw new Error(解密私钥未设置); } try { return sm2.doDecrypt(cipherTextHex, privateKey, this.cipherMode); } catch (error) { console.error(SM2解密失败:, error); // 解密失败可能是密文被篡改或密钥不匹配属于安全事件应记录日志并向上抛出 throw new Error(解密失败请检查密文或密钥是否正确); } } /** * 生成签名默认使用SM3哈希 * param {string} message - 原始消息 * param {string} privateKey - 可选若不传则使用实例私钥 * param {object} options - 签名选项默认 { hash: true } * returns {string} 十六进制签名 */ sign(message, privateKey this.privateKey, options { hash: true }) { if (!privateKey) { throw new Error(签名私钥未设置); } try { // 确保选项一致性特别是publicKey在需要时应传入 const signOptions { ...options }; return sm2.doSignature(message, privateKey, signOptions); } catch (error) { console.error(SM2签名失败:, error); throw new Error(签名失败: ${error.message}); } } /** * 验证签名 * param {string} message - 原始消息 * param {string} signatureHex - 十六进制签名 * param {string} publicKey - 可选若不传则使用实例公钥 * param {object} options - 必须与签名时一致默认 { hash: true } * returns {boolean} 验证是否通过 */ verify(message, signatureHex, publicKey this.publicKey, options { hash: true }) { if (!publicKey) { throw new Error(验签公钥未设置); } try { const verifyOptions { ...options }; return sm2.doVerifySignature(message, signatureHex, publicKey, verifyOptions); } catch (error) { console.error(SM2验签失败:, error); // 验签过程出错非签名无效返回false更安全 return false; } } /** * 便捷方法加密JSON对象自动序列化 */ encryptJSON(obj, publicKey this.publicKey) { const jsonString JSON.stringify(obj); return this.encrypt(jsonString, publicKey); } /** * 便捷方法解密并解析JSON对象 */ decryptJSON(cipherTextHex, privateKey this.privateKey) { const jsonString this.decrypt(cipherTextHex, privateKey); try { return JSON.parse(jsonString); } catch (e) { throw new Error(解密结果不是有效的JSON字符串); } } /** * 静态方法生成新的密钥对 * returns {{publicKey: string, privateKey: string}} */ static generateKeyPair() { return sm2.generateKeyPairHex(); } } export default SM2Crypto;5.2 封装的核心考量错误处理原始的sm2.doEncrypt在密钥格式错误时可能抛出难以理解的异常。封装类里用try...catch包裹抛出业务语义更清晰的错误便于上层调用者处理。配置集中管理将加密模式cipherMode、公钥publicKey等配置项集中在构造函数或设置方法中避免在业务代码中散落。公钥通常从后端接口获取可以很方便地通过setPublicKey更新。密钥生命周期前端通常只持有公钥用于加密私钥应安全地存储在后端。封装类支持不传入私钥这样即使工具类被不小心暴露风险也相对可控。签名功能需谨慎使用确保私钥来源安全通常用于本地生成临时密钥对场景而非固定私钥。便捷方法添加了encryptJSON/decryptJSON方法因为前端传输的数据大多是JSON对象这个方法自动处理序列化和反序列化提升开发效率。静态方法将密钥对生成generateKeyPair作为静态方法逻辑清晰表明它不依赖于实例状态。5.3 在项目中使用// 1. 初始化通常公钥从后端接口获取 import SM2Crypto from /utils/sm2Crypto; const cryptoUtil new SM2Crypto(后端下发的公钥...); // 2. 加密用户密码示例 async function handleLogin(username, password) { const encryptedPassword cryptoUtil.encrypt(password); // 将 username 和 encryptedPassword 发送到后端 const response await fetch(/api/login, { method: POST, body: JSON.stringify({ username, password: encryptedPassword }), headers: { Content-Type: application/json } }); // ... 处理响应 } // 3. 加密复杂数据 const orderData { userId: 123, amount: 100.0, productId: P001 }; const encryptedOrder cryptoUtil.encryptJSON(orderData); // 直接发送 encryptedOrder 字符串 // 4. 动态更新公钥 cryptoUtil.setPublicKey(newPublicKeyFromApi);6. 联调与对接实战前后端一致的“生命线”前端加密只是半场必须和后端解密成功匹配才行。联调阶段是问题高发区下面是我总结的必须对齐的 checklist。6.1 加密模式 (Cipher Mode)这是最大的一个坑SM2加密后的密文由三部分组成C1 (椭圆曲线点)、C2 (密文)、C3 (杂凑值)。这三者的拼接顺序有两种标准C1C2C3旧标准有些较老的库或系统使用。C1C3C2新标准也是sm-crypto默认模式cipherMode1目前更常见尤其是与JavaBouncyCastle库对接时。必须与后端确认他们使用的SM2库期望哪种顺序sm-crypto的doEncrypt和doDecrypt的第三个参数就是用来指定这个的0代表C1C2C31代表C1C3C2。前后端不一致会导致解密失败。6.2 公钥格式前端生成的或后端提供的公钥其格式必须双方认可。sm-crypto生成和使用的公钥是04 X Y的130位十六进制字符串04表示未压缩。这是最通用的格式。后端可能的要求有时后端会要求传入“裸公钥”即去掉04仅XY的128位十六进制或者要求是Base64编码的。务必确认。处理建议在工具类中可以添加一个公钥格式化的方法根据后端要求进行转换。// 例如如果后端需要裸公钥 function getRawPublicKey(standardPublicKeyWith04) { if (standardPublicKeyWith04.startsWith(04)) { return standardPublicKeyWith04.slice(2); } return standardPublicKeyWith04; } // 加密时使用格式化后的公钥 const rawPublicKey getRawPublicKey(backendPublicKey); const encrypted sm2.doEncrypt(data, rawPublicKey, 1);6.3 数据编码与填充明文编码sm-crypto的doEncrypt方法接受字符串明文内部会将其转换为UTF-8字节进行处理。确保你传入的字符串编码是预期的。对于特殊字符保持一致即可。密文输出doEncrypt输出的是十六进制字符串。如果你需要Base64格式比如为了节省传输体积需要自行转换。const cipherHex sm2.doEncrypt(data, publicKey, 1); const cipherBase64 btoa(cipherHex.match(/\w{2}/g).map(byte String.fromCharCode(parseInt(byte, 16))).join()); // 注意btoa 处理ASCII这里简单演示生产环境建议使用规范的 hex to base64 库填充方案SM2加密本身涉及密钥派生函数(KDF)和对称加密部分sm-crypto内部已经处理了填充。通常不需要前端额外关心但要知道后端使用的是哪种KDF通常是SM3。6.4 签名验签参数如果用到签名需确认哈希算法sm2.doSignature的options中hash: true表示先对消息做SM3哈希再签名这是推荐且常见的做法。确保后端验签时也采用同样的哈希流程。UserIdSM2签名标准中有一个可选的用户标识符(UserId)默认是空字符串对应十六进制0x31323334353637383132333435363738即ASCII码“1234567812345678”的十六进制。绝大多数情况下前后端都使用默认空字符串即可。但如果后端明确指定了UserId前端必须在签名和验签的options中传入相同的userId参数否则验签会失败。const options { hash: true, userId: yourUserIdsystem }; const sig sm2.doSignature(msg, privateKey, options); const ok sm2.doVerifySignature(msg, sig, publicKey, options); // 必须使用相同的options6.5 联调检查清单与后端联调前可以准备这样一份清单进行确认[ ]加密/解密模式C1C2C3 (0) 还是 C1C3C2 (1)我们约定使用1。[ ]公钥格式带04的130位Hex还是128位裸Hex还是Base64我们约定使用04开头的130位十六进制字符串。[ ]私钥格式64位Hex我们约定使用64位十六进制字符串。[ ]签名/验签哈希是否先进行SM3哈希我们约定hash: true。[ ]签名UserId是否使用默认空字符串我们约定使用默认空字符串。[ ]密文传输格式Hex还是Base64我们约定使用十六进制字符串。7. 常见问题排查与性能优化即使按照指南操作在实际开发中还是会遇到一些“诡异”的问题。这里记录了几个我踩过的坑和解决方案。7.1 问题加密后后端解密失败报“Invalid ciphertext”或“解密错误”排查步骤确认加密模式这是最常见的原因。用同一个密钥对让后端用两种模式0和1都尝试解密一下。或者前端写一个简单的Node.js测试脚本用sm-crypto加密然后用后端的Java/Python代码解密快速验证模式。检查公钥确保传给doEncrypt的公钥字符串完全正确没有多余的空格、换行或误编码。特别是从配置文件或接口获取时注意去除首尾空白。检查密文传输确保加密生成的十六进制字符串在通过网络传输如HTTP请求时没有被意外转换。比如如果放在URL参数里要确保正确编码放在JSON body里要确保是字符串类型。数据长度SM2算法本身对明文长度有限制与密钥长度有关。sm-crypto在加密长文本时内部会采用分组加密。但如果你加密的数据量极大比如上MB可能会遇到性能问题或内存溢出。对于超长数据通常建议采用“SM2加密随机对称密钥再用SM4加密数据”的混合加密模式但这需要后端也做相应配合。7.2 问题签名验签不通过排查步骤严格比对options确保签名和验签时传入的options对象完全一致特别是hash和userId字段。一个字符都不能差。消息一致性验证时使用的原始消息必须和签名时完全一样。注意空格、不可见字符、编码如中文是UTF-8等问题。建议在签名前对消息字符串进行一次标准化处理如.trim()。密钥对应关系验签的公钥必须和签名的私钥是配对的。有时候不小心用错了密钥对。7.3 问题在Vue/React组件中重复初始化工具类导致性能浪费解决方案使用单例模式或依赖注入。在Vue中可以将其挂载到Vue原型上或使用provide/inject在React中可以使用Context或自定义Hook。// Vue3示例 (在main.js或插件中) import { createApp } from vue; import App from ./App.vue; import SM2Crypto from ./utils/sm2Crypto; const app createApp(App); // 从环境变量或配置中心获取公钥 const publicKey import.meta.env.VITE_SM2_PUBLIC_KEY; const sm2Crypto new SM2Crypto(publicKey); app.config.globalProperties.$sm2 sm2Crypto; // 在组件中使用this.$sm2.encrypt(...) // 或者使用Provide/Inject app.provide(sm2Crypto, sm2Crypto);7.4 性能考量与优化加密性能对于单次加密如密码、令牌、关键字段SM2的性能开销可以忽略不计。但如果需要批量加密大量数据如列表中的每条记录可能会引起界面卡顿。优化建议Web Worker将加密解密操作放入Web Worker避免阻塞主线程。这对于需要实时加密大量数据的应用如聊天非常有效。懒加载由于sm-crypto有一定体积如果并非所有用户都需要加密功能可以考虑动态导入。// 在需要的时候再加载 async function encryptData(data) { const { sm2 } await import(sm-crypto); return sm2.doEncrypt(data, publicKey, 1); }避免重复加密对于相同的数据如果公钥不变加密结果也是确定的。可以考虑在短时间内在内存中缓存加密结果但要注意数据敏感性。7.5 安全性提醒非常重要前端加密不等于绝对安全前端代码是公开的公钥也是公开的。前端加密的主要作用是防止数据在传输过程中被窃听即TLS之外的额外保护并确保数据来自可信客户端结合签名。绝不能依赖前端加密来隐藏业务逻辑或替代后端权限验证。私钥绝不能在前端硬编码或泄露用于解密的私钥必须牢牢掌握在后端。前端持有的私钥只应用于临时场景如生成临时密钥对或非敏感操作的签名。使用HTTPS前端加密必须建立在HTTPSTLS的基础上形成双重保障。没有HTTPS攻击者可以轻易篡改你加载的JavaScript文件替换掉公钥。密钥管理公钥也应通过安全渠道如HTTPS接口从后端动态获取而不是硬编码在代码中方便定期轮换。整个流程走下来从最初的不知所措到最终平滑上线最大的体会就是细节决定成败。国密算法的对接难点往往不在算法本身而在前后端对诸多参数和格式的约定上。希望这份从安装、封装到联调的完整指南能帮你扫清障碍把SM2国密算法稳稳地集成到你的前端项目中。如果在实际操作中遇到新的问题不妨回头检查一下加密模式、密钥格式和签名参数这几个核心点大概率能找到答案。

相关新闻