q-koa 13.8.32 → 13.8.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/app.js CHANGED
@@ -231,7 +231,6 @@ class APP {
231
231
  * 挂在jwt解密方法
232
232
  */
233
233
  if (this.app[appName]) {
234
- console.time('ss')
235
234
  this.app[appName].sign = async (user) => {
236
235
  const result = await jwt.sign(user, secret, {
237
236
  expiresIn,
@@ -239,7 +238,6 @@ class APP {
239
238
  })
240
239
  return result
241
240
  }
242
- console.timeEnd('ss')
243
241
  }
244
242
  // eslint-disable-next-line no-nested-ternary
245
243
  ctx.request.clientType = ctx.header['client-type']
@@ -82,6 +82,54 @@ const sendWxFail = (ctx, msg = 'FAIL') => {
82
82
  )
83
83
  }
84
84
 
85
+ /**
86
+ * 通联(AllinPay)融合回调签名验证(RSA / MD5)
87
+ * @param {Object} options
88
+ * @param {Object} options.notifyData 回调 body(必须包含 sign 字段)
89
+ * @param {string} options.signType 'RSA' | 'MD5'(已大写)
90
+ * @param {string} options.publicKey 通联平台公钥(PEM body,无 header/footer)
91
+ * @param {string} options.md5Key MD5 签名密钥(signType=MD5 时使用)
92
+ * @returns {boolean}
93
+ */
94
+ const validateUnionNotifySign = ({ notifyData, signType, publicKey, md5Key }) => {
95
+ if (!notifyData || !notifyData.sign) return false
96
+ const params = { ...notifyData }
97
+ const sign = String(params.sign)
98
+ delete params.sign
99
+ delete params.signType // signType 本身不参与签名原文
100
+
101
+ // 通联 buildSignStr:按 ASCII 字典序拼接,跳过空/undefined/null
102
+ const sortedKeys = Object.keys(params).sort()
103
+ let signStr = ''
104
+ for (const k of sortedKeys) {
105
+ const v = params[k]
106
+ if (v === undefined || v === null || v === '') continue
107
+ if (signStr) signStr += '&'
108
+ signStr += `${k}=${v}`
109
+ }
110
+
111
+ if (signType === 'MD5') {
112
+ if (!md5Key) return false
113
+ const expected = crypto
114
+ .createHash('md5')
115
+ .update(signStr + `&key=${md5Key}`, 'utf8')
116
+ .digest('hex')
117
+ .toUpperCase()
118
+ return expected === sign.toUpperCase()
119
+ }
120
+
121
+ // 默认 RSA-SHA1(与 allinpay.js verifySign 保持一致)
122
+ if (!publicKey) return false
123
+ try {
124
+ const pem = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`
125
+ const verify = crypto.createVerify('RSA-SHA1')
126
+ verify.update(signStr, 'utf8')
127
+ return verify.verify(pem, sign, 'base64')
128
+ } catch (e) {
129
+ return false
130
+ }
131
+ }
132
+
85
133
  const check = ({ timestamp, nonce, signature, token }) => {
86
134
  const tmp = [token, timestamp, nonce].sort().join('')
87
135
  const currSign = crypto.createHash('sha1').update(tmp).digest('hex')
@@ -1453,11 +1501,74 @@ exports.union_notify = async (ctx) => {
1453
1501
  try {
1454
1502
  const notifyData = ctx.request.body
1455
1503
 
1504
+ // TODO: 临时观察日志,上线后删除
1505
+ app.service.log.push({
1506
+ app,
1507
+ message: `通联融合回调union_notify报文: ${JSON.stringify(notifyData || {})}`,
1508
+ })
1509
+
1510
+ if (!notifyData || typeof notifyData !== 'object') {
1511
+ ctx.status = 400
1512
+ ctx.body = 'FAIL'
1513
+ return
1514
+ }
1515
+
1516
+ // ===== 通联(AllinPay)回调验签 + 商户号绑定 =====
1517
+ const pay_config = 'weixin_pay'
1518
+ const appConfig = getConfig(app)
1519
+ const {
1520
+ union_mchId,
1521
+ union_appid,
1522
+ union_publicKey,
1523
+ union_md5Key,
1524
+ union_signType,
1525
+ } = await appConfig.getObject(pay_config)
1526
+ const cusid = notifyData.cusid
1527
+
1528
+ if (cusid && union_mchId && cusid !== union_mchId) {
1529
+ app.service.log.push({
1530
+ app,
1531
+ message: `通联回调商户号不匹配: cfg=${union_mchId} got=${cusid}`,
1532
+ })
1533
+ ctx.body = 'FAIL'
1534
+ return
1535
+ }
1536
+
1537
+ // 优先按回调带的 signType 决定算法,没有则退到配置 union_signType,再退 RSA
1538
+ const rawSignType = (notifyData.signType || union_signType || 'RSA').toUpperCase()
1539
+ const hasSign = notifyData.sign !== undefined && notifyData.sign !== ''
1540
+ const trxstatus = notifyData.trxstatus
1541
+
1542
+ // 仅当成功状态(trxstatus==='0000')强制要求签名;失败状态没有 sign 时仍打日志但直接返回 success(不重放)
1543
+ if (trxstatus === '0000' && !hasSign) {
1544
+ app.service.log.push({
1545
+ app,
1546
+ message: `通联回调签名缺失: ${JSON.stringify(notifyData)}`,
1547
+ })
1548
+ ctx.body = 'FAIL'
1549
+ return
1550
+ }
1551
+
1552
+ if (hasSign) {
1553
+ const signOk = validateUnionNotifySign({
1554
+ notifyData,
1555
+ signType: rawSignType,
1556
+ publicKey: union_publicKey,
1557
+ md5Key: union_md5Key,
1558
+ })
1559
+ if (!signOk) {
1560
+ app.service.log.push({
1561
+ app,
1562
+ message: `通联回调验签失败(type=${rawSignType}): ${JSON.stringify(notifyData)}`,
1563
+ })
1564
+ ctx.body = 'FAIL'
1565
+ return
1566
+ }
1567
+ }
1568
+
1456
1569
  const {
1457
1570
  trxcode,
1458
- trxstatus,
1459
1571
  cusorderid,
1460
- cusid,
1461
1572
  acct,
1462
1573
  trxid,
1463
1574
  trxamt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "q-koa",
3
- "version": "13.8.32",
3
+ "version": "13.8.33",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {