如何用单PEM证书在NodeJS的node-soap中调用带SSL认证的SOAP API
问题:使用node-soap通过单PEM文件调用带SSL认证的SOAP服务
背景
已有可正常运行的PHP代码,通过单PEM文件实现SSL客户端认证并调用SOAP服务:
<?php $certificate = 'demo/certificate.pem'; $location = '<serverurl>'; $passphrase = '<passphrase>'; $wsdl = 'demo/Api.wsdl'; $soapClient = new SoapClient( $wsdl, [ 'local_cert' => $certificate, 'location' => $location, 'passphrase' => $passphrase, 'stream_context' => stream_context_create([ 'http' => ['user_agent' => 'PHP/SOAP'], 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, ], ]), ] ); // 调用SOAP方法 $result = $soapClient->Check([ 'checkReq' => [ 'operation' => 1, 'voucherCode' => 'abcd1234', ], ]); print_r($result);
尝试用node-soap实现时,因库要求分开的私钥和证书文件,手动拆分PEM后出现解密错误:
const https = require("https") const fs = require("fs") const axios = require("axios") const soap = require("soap") class ApiWrapper { async getClient() { const privateKey = fs.readFileSync('./demo/private.pem', 'utf-8') const publicCertificate = fs.readFileSync('./demo/public.pem', 'utf-8') const password = '<passphrase>' const url = 'demo/Api.wsdl' const httpsAgent = new https.Agent({ key: privateKey, cert: publicCertificate }) const wsdlRequest = await axios.create({ httpsAgent }) let client = await soap.createClientAsync( url, { request: wsdlRequest, wsdl_options: { connection: 'keep-alive' } } ) const wsSecurity = new soap.WSSecurityCert( privateKey, publicCertificate, password ) client.setSecurity(wsSecurity) return client } } async function main() { const wrapper = new ApiWrapper() const client = await wrapper.getClient() const result = await client.CheckAsync() console.log({result}) } main()
错误信息
Error: error:1C800064:Provider routines::bad decrypt at Sign.sign (node:internal/crypto/sig:128:29) at RSASHA1.getSignature (/<path_to_project>/node_modules/xml-crypto/lib/signed-xml.js:70:22) at SignedXml.calculateSignatureValue (/<path_to_project>/node_modules/xml-crypto/lib/signed-xml.js:453:32) at SignedXml.computeSignature (/<path_to_project>/node_modules/xml-crypto/lib/signed-xml.js:865:10) at WSSecurityCert.postProcess (/<path_to_project>/node_modules/soap/lib/security/WSSecurityCert.js:141:21) at Client._invoke (/<path_to_project>/node_modules/soap/lib/client.js:357:33) at /<path_to_project>/node_modules/soap/lib/client.js:187:18 at /<path_to_project>/node_modules/soap/lib/client.js:165:17 at new Promise (<anonymous>) at Client.CheckAsync (/<path_to_project>/node_modules/soap/lib/client.js:156:20) { opensslErrorStack: [ 'error:11800074:PKCS12 routines::pkcs12 cipherfinal error', 'error:1C800064:Provider routines::bad decrypt', 'error:11800074:PKCS12 routines::pkcs12 cipherfinal error' ], library: 'Provider routines', reason: 'bad decrypt', code: 'ERR_OSSL_BAD_DECRYPT' }
解决方案
核心问题:混淆了SSL客户端认证与WS-Security签名
PHP代码中使用的是SSL层面的客户端证书认证(通过local_cert配置),而node-soap的WSSecurityCert是用于WS-Security协议的XML内容签名,二者完全不同。正确的做法是使用ClientSSLSecurity实现SSL客户端认证,无需手动拆分PEM文件。
完整实现代码
const fs = require('fs'); const soap = require('soap'); const https = require('https'); const axios = require('axios'); class ApiWrapper { async getClient() { const pemPath = './demo/certificate.pem'; const passphrase = '<passphrase>'; const wsdlUrl = 'demo/Api.wsdl'; const serverUrl = '<serverurl>'; // 读取单PEM文件内容 const pemContent = fs.readFileSync(pemPath, 'utf-8'); // 从PEM中提取私钥和证书(兼容RSA PRIVATE KEY和PRIVATE KEY格式) const privateKey = pemContent.match(/-----BEGIN (?:RSA )?PRIVATE KEY-----[\s\S]+?-----END (?:RSA )?PRIVATE KEY-----/)[0]; const publicCert = pemContent.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/)[0]; // 配置HTTPS Agent,对应PHP中的stream_context const httpsAgent = new https.Agent({ key: privateKey, cert: publicCert, passphrase: passphrase, // 对应PHP的verify_peer=false,生产环境建议开启并配置可信CA rejectUnauthorized: false }); // 创建带HTTPS Agent的请求实例,用于WSDL获取和SOAP调用 const axiosInstance = axios.create({ httpsAgent }); // 创建SOAP客户端 const client = await soap.createClientAsync(wsdlUrl, { request: axiosInstance, wsdl_options: { connection: 'keep-alive' }, // 指定SOAP请求的目标地址,对应PHP的location参数 endpoint: serverUrl }); // 使用ClientSSLSecurity实现SSL客户端认证,与PHP逻辑一致 const sslSecurity = new soap.ClientSSLSecurity( privateKey, publicCert, { passphrase: passphrase, rejectUnauthorized: false } ); client.setSecurity(sslSecurity); return client; } } async function main() { try { const wrapper = new ApiWrapper(); const client = await wrapper.getClient(); // 传递与PHP相同的参数 const [result] = await client.CheckAsync({ checkReq: { operation: 1, voucherCode: 'abcd1234' } }); console.log('SOAP响应:', result); } catch (error) { console.error('调用失败:', error); } } main();
关键说明
- PEM内容提取:通过正则从单PEM文件中自动拆分私钥和证书,避免手动拆分时的格式错误。
- 正确选择安全类:使用
ClientSSLSecurity而非WSSecurityCert,匹配PHP的SSL客户端认证逻辑。 - 参数对应:
rejectUnauthorized: false对应PHP的verify_peer: false和verify_peer_name: falseendpoint参数对应PHP的location- 调用
CheckAsync时需传递与PHP一致的请求参数
注意事项
- 生产环境中请勿关闭
rejectUnauthorized,应配置可信CA证书以保障安全。 - 若服务端确实需要WS-Security签名,需确保私钥和证书提取正确,且密码无误,再使用
WSSecurityCert配置。
内容的提问来源于stack exchange,提问作者mrodo
相关产品推荐
相关产品推荐

