alipayBase.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. const {
  2. camel2snakeJson,
  3. snake2camelJson,
  4. getOffsetDate,
  5. getFullTimeStr
  6. } = require('../../../common/utils')
  7. const crypto = require('crypto')
  8. const ALIPAY_ALGORITHM_MAPPING = {
  9. RSA: 'RSA-SHA1',
  10. RSA2: 'RSA-SHA256'
  11. }
  12. module.exports = class AlipayBase {
  13. constructor (options = {}) {
  14. if (!options.appId) throw new Error('appId required')
  15. if (!options.privateKey) throw new Error('privateKey required')
  16. const defaultOptions = {
  17. gateway: 'https://openapi.alipay.com/gateway.do',
  18. timeout: 5000,
  19. charset: 'utf-8',
  20. version: '1.0',
  21. signType: 'RSA2',
  22. timeOffset: -new Date().getTimezoneOffset() / 60,
  23. keyType: 'PKCS8'
  24. }
  25. if (options.sandbox) {
  26. options.gateway = 'https://openapi.alipaydev.com/gateway.do'
  27. }
  28. this.options = Object.assign({}, defaultOptions, options)
  29. const privateKeyType =
  30. this.options.keyType === 'PKCS8' ? 'PRIVATE KEY' : 'RSA PRIVATE KEY'
  31. this.options.privateKey = this._formatKey(
  32. this.options.privateKey,
  33. privateKeyType
  34. )
  35. if (this.options.alipayPublicKey) {
  36. this.options.alipayPublicKey = this._formatKey(
  37. this.options.alipayPublicKey,
  38. 'PUBLIC KEY'
  39. )
  40. }
  41. }
  42. _formatKey (key, type) {
  43. return `-----BEGIN ${type}-----\n${key}\n-----END ${type}-----`
  44. }
  45. _formatUrl (url, params) {
  46. let requestUrl = url
  47. // 需要放在 url 中的参数列表
  48. const urlArgs = [
  49. 'app_id',
  50. 'method',
  51. 'format',
  52. 'charset',
  53. 'sign_type',
  54. 'sign',
  55. 'timestamp',
  56. 'version',
  57. 'notify_url',
  58. 'return_url',
  59. 'auth_token',
  60. 'app_auth_token'
  61. ]
  62. for (const key in params) {
  63. if (urlArgs.indexOf(key) > -1) {
  64. const val = encodeURIComponent(params[key])
  65. requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?'
  66. }${key}=${val}`
  67. // 删除 postData 中对应的数据
  68. delete params[key]
  69. }
  70. }
  71. return { execParams: params, url: requestUrl }
  72. }
  73. _getSign (method, params) {
  74. const bizContent = params.bizContent || null
  75. delete params.bizContent
  76. const signParams = Object.assign({
  77. method,
  78. appId: this.options.appId,
  79. charset: this.options.charset,
  80. version: this.options.version,
  81. signType: this.options.signType,
  82. timestamp: getFullTimeStr(getOffsetDate(this.options.timeOffset))
  83. }, params)
  84. if (bizContent) {
  85. signParams.bizContent = JSON.stringify(camel2snakeJson(bizContent))
  86. }
  87. // params key 驼峰转下划线
  88. const decamelizeParams = camel2snakeJson(signParams)
  89. // 排序
  90. const signStr = Object.keys(decamelizeParams)
  91. .sort()
  92. .map((key) => {
  93. let data = decamelizeParams[key]
  94. if (Array.prototype.toString.call(data) !== '[object String]') {
  95. data = JSON.stringify(data)
  96. }
  97. return `${key}=${data}`
  98. })
  99. .join('&')
  100. // 计算签名
  101. const sign = crypto
  102. .createSign(ALIPAY_ALGORITHM_MAPPING[this.options.signType])
  103. .update(signStr, 'utf8')
  104. .sign(this.options.privateKey, 'base64')
  105. return Object.assign(decamelizeParams, { sign })
  106. }
  107. async _exec (method, params = {}, option = {}) {
  108. // 计算签名
  109. const signData = this._getSign(method, params)
  110. const { url, execParams } = this._formatUrl(this.options.gateway, signData)
  111. const { status, data } = await uniCloud.httpclient.request(url, {
  112. method: 'POST',
  113. data: execParams,
  114. // 按 text 返回(为了验签)
  115. dataType: 'text',
  116. timeout: this.options.timeout
  117. })
  118. if (status !== 200) throw new Error('request fail')
  119. /**
  120. * 示例响应格式
  121. * {"alipay_trade_precreate_response":
  122. * {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"},
  123. * "sign": "abcde="
  124. * }
  125. * 或者
  126. * {"error_response":
  127. * {"code":"40002","msg":"Invalid Arguments","sub_code":"isv.code-invalid","sub_msg":"授权码code无效"},
  128. * }
  129. */
  130. const result = JSON.parse(data)
  131. const responseKey = `${method.replace(/\./g, '_')}_response`
  132. const response = result[responseKey]
  133. const errorResponse = result.error_response
  134. if (response) {
  135. // 按字符串验签
  136. const validateSuccess = option.validateSign ? this._checkResponseSign(data, responseKey) : true
  137. if (validateSuccess) {
  138. if (!response.code || response.code === '10000') {
  139. const errCode = 0
  140. const errMsg = response.msg || ''
  141. return {
  142. errCode,
  143. errMsg,
  144. ...snake2camelJson(response)
  145. }
  146. }
  147. const msg = response.sub_code ? `${response.sub_code} ${response.sub_msg}` : `${response.msg || 'unkonwn error'}`
  148. throw new Error(msg)
  149. } else {
  150. throw new Error('check sign error')
  151. }
  152. } else if (errorResponse) {
  153. throw new Error(errorResponse.sub_msg || errorResponse.msg || 'request fail')
  154. }
  155. throw new Error('request fail')
  156. }
  157. _checkResponseSign (signStr, responseKey) {
  158. if (!this.options.alipayPublicKey || this.options.alipayPublicKey === '') {
  159. console.warn('options.alipayPublicKey is empty')
  160. // 支付宝公钥不存在时不做验签
  161. return true
  162. }
  163. // 带验签的参数不存在时返回失败
  164. if (!signStr) { return false }
  165. // 根据服务端返回的结果截取需要验签的目标字符串
  166. const validateStr = this._getSignStr(signStr, responseKey)
  167. // 服务端返回的签名
  168. const serverSign = JSON.parse(signStr).sign
  169. // 参数存在,并且是正常的结果(不包含 sub_code)时才验签
  170. const verifier = crypto.createVerify(ALIPAY_ALGORITHM_MAPPING[this.options.signType])
  171. verifier.update(validateStr, 'utf8')
  172. return verifier.verify(this.options.alipayPublicKey, serverSign, 'base64')
  173. }
  174. _getSignStr (originStr, responseKey) {
  175. // 待签名的字符串
  176. let validateStr = originStr.trim()
  177. // 找到 xxx_response 开始的位置
  178. const startIndex = originStr.indexOf(`${responseKey}"`)
  179. // 找到最后一个 “"sign"” 字符串的位置(避免)
  180. const lastIndex = originStr.lastIndexOf('"sign"')
  181. /**
  182. * 删除 xxx_response 及之前的字符串
  183. * 假设原始字符串为
  184. * {"xxx_response":{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"}
  185. * 删除后变为
  186. * :{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"}
  187. */
  188. validateStr = validateStr.substr(startIndex + responseKey.length + 1)
  189. /**
  190. * 删除最后一个 "sign" 及之后的字符串
  191. * 删除后变为
  192. * :{"code":"10000"},
  193. * {} 之间就是待验签的字符串
  194. */
  195. validateStr = validateStr.substr(0, lastIndex)
  196. // 删除第一个 { 之前的任何字符
  197. validateStr = validateStr.replace(/^[^{]*{/g, '{')
  198. // 删除最后一个 } 之后的任何字符
  199. validateStr = validateStr.replace(/\}([^}]*)$/g, '}')
  200. return validateStr
  201. }
  202. }