diff --git a/sop-sdk/sdk-nodejs-axios/common/OpenClient.js b/sop-sdk/sdk-nodejs-axios/common/OpenClient.js new file mode 100644 index 00000000..d242a096 --- /dev/null +++ b/sop-sdk/sdk-nodejs-axios/common/OpenClient.js @@ -0,0 +1,183 @@ +const axios = require('axios'); +const formData = require('form-data'); +const moment = require('moment'); +const qs = require('qs'); + +const {RequestType} = require('./RequestType'); +const {SignUtil} = require('./SignUtil'); +const {BaseRequest} = require('../request/BaseRequest'); + +const HEADERS = {'Accept-Encoding': 'identity'}; + +const getHeaders = (headers = {}) => { + return Object.assign({}, headers, HEADERS); +}; + +const parseResponse = (error, response, request) => { + if (!error && response.status === 200) { + return request.parseResponse(response.data); + } else { + return { // 重新封装请求异常回调,以防中断 + msg: '请求异常', + code: '502', + sub_msg: `${error}`, + sub_code: 'isv.invalid-server' + }; + } +}; + +const buildParams = (instance, request, token) => { + const {appId, privateKey} = instance; + const allParams = { + 'app_id': appId, + 'method': request.getMethod(), + 'charset': 'UTF-8', + 'sign_type': 'RSA2', + 'timestamp': moment().format('YYYY-MM-DD HH:mm:ss'), + 'version': request.getVersion(), + 'biz_content': JSON.stringify(request.bizModel) + }; + + if (token) { + allParams['app_auth_token'] = token; + } + // 创建签名 + allParams.sign = SignUtil.createSign(allParams, privateKey, 'RSA2'); + return allParams; +}; + +const executeRequest = async (instance = {}, request, token, callback, {headers}) => { + const params = buildParams(instance, request, token); + const {url} = instance; + const options = { + url, + method: 'POST', + params: undefined, + data: undefined + }; + headers = getHeaders(headers); + const requestType = request.getRequestType(); + switch (requestType) { + case RequestType.GET: { + options.method = 'GET'; + options.params = params; + } + break; + case RequestType.POST_FORM: { + headers = Object.assign(headers, { + 'Content-Type': 'application/x-www-form-urlencoded' + }); + options.data = qs.stringify(params); + } + break; + case RequestType.POST_JSON: { + options.data = params; + } + break; + case RequestType.POST_FILE: { + const formData = new formData(); + (request.files || []).forEach(({name, path}) => { + formData.append(name, path, { + contentType: 'application/octet-stream' + }); + }); + Object.keys(params).forEach(key => { + const value = params[key]; + if (!(typeof key === 'undefined' || typeof value === 'undefined')) { + formData.append(key, params[key]); + } + }); + options.data = formData; + } + break; + default: { + callback(parseResponse(new Error('request.getRequestType()类型不正确'), undefined, request)); + return; + } + } + try { + options['headers'] = headers; + const response = await axios.request(options); + callback(parseResponse(undefined, response, request)); + } catch (error) { + callback(parseResponse(error, undefined, request)); + } +}; + +module.exports = class OpenClient { + /** + * 初始化客户端 + * @param appId 应用ID + * @param privateKey 应用私钥,2048位,PKCS8 + * @param url 请求url + */ + constructor(appId, privateKey, url) { + this.appId = appId; + this.privateKey = privateKey; + this.url = url; + } + + /** + * 发送请求 + * @param request 请求类 + * @param callback 回调函数,参数json(undefined则使用executeSync) + * @param options 自定义参数,如headers + */ + execute(request, callback, options) { + if (typeof callback == 'function') { + return this.executeToken(request, null, callback, options); + } else { + return this.executeSync(request, options); + } + } + + /** + * 发送同步请求 + * @param request 请求类 + * @param options 自定义参数,如headers + * */ + executeSync(request, options) { + return new Promise((resolve) => { + const _ = this.execute(request, res => { + resolve(res); + }, options); + }); + } + + /** + * 发送请求 + * @param request 请求类 + * @param token token + * @param callback 回调函数,参数json(undefined则使用executeTokenSync) + * @param options 自定义参数,如headers + */ + async executeToken(request, token, callback, options) { + if (!(request instanceof BaseRequest)) { + throw 'request类未继承BaseRequest'; + } + if (typeof callback == 'function') { + const files = request.files; + if (files && files.length > 0) { + request.setForceRequestType(RequestType.POST_FILE); + } + return await executeRequest(this, request, token, callback, options); + } else { + return this.executeTokenSync(request, token); + } + } + + /** + * 发送同步请求 + * @param request 请求类 + * @param token token + * @param options 自定义参数,如headers + */ + executeTokenSync(request, token, options) { + return new Promise((resolve) => { + const _ = this.executeToken(request, token, res => { + resolve(res); + }, options); + }); + } + +}; \ No newline at end of file diff --git a/sop-sdk/sdk-nodejs-axios/common/RequestType.js b/sop-sdk/sdk-nodejs-axios/common/RequestType.js new file mode 100644 index 00000000..717d99f5 --- /dev/null +++ b/sop-sdk/sdk-nodejs-axios/common/RequestType.js @@ -0,0 +1,6 @@ +exports.RequestType = { + GET: 'GET', + POST_FORM: 'POST_FORM', + POST_JSON: 'POST_JSON', + POST_FILE: 'POST_FILE' +}; diff --git a/sop-sdk/sdk-nodejs-axios/common/SignUtil.js b/sop-sdk/sdk-nodejs-axios/common/SignUtil.js new file mode 100644 index 00000000..453b3dbe --- /dev/null +++ b/sop-sdk/sdk-nodejs-axios/common/SignUtil.js @@ -0,0 +1,88 @@ +const {KJUR, hextob64} = require('jsrsasign'); + +const HashMap = { + SHA256withRSA: 'SHA256withRSA', + SHA1withRSA: 'SHA1withRSA' +}; + +const PEM_BEGIN = '-----BEGIN PRIVATE KEY-----\n'; +const PEM_END = '\n-----END PRIVATE KEY-----'; + +/** + * rsa签名参考:https://www.jianshu.com/p/145eab95322c + */ +exports.SignUtil = { + /** + * 创建签名 + * @param params 请求参数 + * @param privateKey 私钥,PKCS8 + * @param signType 签名类型,RSA,RSA2 + * @returns 返回签名内容 + */ + createSign(params, privateKey, signType) { + const content = this.getSignContent(params); + return this.sign(content, privateKey, signType); + }, + sign: function (content, privateKey, signType) { + if (signType.toUpperCase() === 'RSA') { + return this.rsaSign(content, privateKey, HashMap.SHA1withRSA); + } else if (signType.toUpperCase() === 'RSA2') { + return this.rsaSign(content, privateKey, HashMap.SHA256withRSA); + } else { + throw 'signType错误'; + } + }, + /** + * rsa签名 + * @param content 签名内容 + * @param privateKey 私钥 + * @param hash hash算法,SHA256withRSA,SHA1withRSA + * @returns 返回签名字符串,base64 + */ + rsaSign: function (content, privateKey, hash) { + privateKey = this._formatKey(privateKey); + // 创建 Signature 对象 + const signature = new KJUR.crypto.Signature({ + alg: hash, + //!这里指定 私钥 pem! + prvkeypem: privateKey + }); + signature.updateString(content); + const signData = signature.sign(); + // 将内容转成base64 + return hextob64(signData); + }, + _formatKey: function (key) { + if (!key.startsWith(PEM_BEGIN)) { + key = PEM_BEGIN + key; + } + if (!key.endsWith(PEM_END)) { + key = key + PEM_END; + } + return key; + }, + /** + * 获取签名内容 + * @param params 请求参数 + * @returns {string} + */ + getSignContent: function (params) { + const paramNames = []; + for (const key in params) { + paramNames.push(key); + } + + paramNames.sort(); + + const paramNameValue = []; + + for (let i = 0, len = paramNames.length; i < len; i++) { + const paramName = paramNames[i]; + const val = params[paramName]; + if (paramName && val) { + paramNameValue.push(`${paramName}=${val}`); + } + } + return paramNameValue.join('&'); + } +}; diff --git a/sop-sdk/sdk-nodejs-axios/package.json b/sop-sdk/sdk-nodejs-axios/package.json new file mode 100644 index 00000000..88232a74 --- /dev/null +++ b/sop-sdk/sdk-nodejs-axios/package.json @@ -0,0 +1,21 @@ +{ + "name": "sdk-nodejs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.21.1", + "form-data": "^4.0.0", + "isarray": "^2.0.5", + "isobject": "^4.0.0", + "jsrsasign": "^8.0.19", + "moment": "^2.27.0", + "qs": "^6.10.1" + } +} diff --git a/sop-sdk/sdk-nodejs-axios/request/BaseRequest.js b/sop-sdk/sdk-nodejs-axios/request/BaseRequest.js new file mode 100644 index 00000000..a3b5b641 --- /dev/null +++ b/sop-sdk/sdk-nodejs-axios/request/BaseRequest.js @@ -0,0 +1,78 @@ +const isArray = require('isarray'); +/** + * 请求类父类 + */ +exports.BaseRequest = class BaseRequest { + constructor() { + this.bizModel = {}; + + this.files = undefined; + + // 用于文件上传时强制转换成POST_FILE请求 + this.__forceRequestType__ = undefined; + } + + setBizModel(biz = {}) { + this.bizModel = biz; + return this; + } + + setFiles(files) { + this.files = files; + return this; + } + + addFile({name, path}) { + if (name && path) { + if (!isArray(this.files)) { + this.files = []; + } + this.files.push({name, path}); + } + return this; + } + + /** + * 返回接口名称 + */ + getMethod() { + throw `未实现BaseRequest类getMethod()方法`; + } + + /** + * 返回版本号 + */ + getVersion() { + throw '未实现BaseRequest类getVersion()方法'; + } + + /** + * 返回请求类型,使用RequestType.js + */ + getRequestType() { + throw '未实现BaseRequest类getRequestType()方法'; + } + + setForceRequestType(type) { + this.__forceRequestType__ = type; + return this; + } + + getRealRequestType() { + return this.__forceRequestType__ || this.getRequestType(); + } + + /** + * 解析返回结果,子类可以覆盖实现 + * @param responseData 服务器返回内容 + * @returns 返回结果 + */ + parseResponse(responseData) { + let data = responseData['error_response']; + if (!data) { + const dataNodeName = this.getMethod().replace(/\./g, '_') + '_response'; + data = responseData[dataNodeName]; + } + return data; + } +}; \ No newline at end of file