q-koa 13.8.32 → 13.8.34

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,56 @@ 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
+ // 通联回调报文字段是全小写 signtype;同时删除大小写两种做防御
100
+ delete params.signType
101
+ delete params.signtype
102
+
103
+ // 通联 buildSignStr:按 ASCII 字典序拼接,跳过空/undefined/null
104
+ const sortedKeys = Object.keys(params).sort()
105
+ let signStr = ''
106
+ for (const k of sortedKeys) {
107
+ const v = params[k]
108
+ if (v === undefined || v === null || v === '') continue
109
+ if (signStr) signStr += '&'
110
+ signStr += `${k}=${v}`
111
+ }
112
+
113
+ if (signType === 'MD5') {
114
+ if (!md5Key) return false
115
+ const expected = crypto
116
+ .createHash('md5')
117
+ .update(signStr + `&key=${md5Key}`, 'utf8')
118
+ .digest('hex')
119
+ .toUpperCase()
120
+ return expected === sign.toUpperCase()
121
+ }
122
+
123
+ // 默认 RSA-SHA1(与 allinpay.js verifySign 保持一致)
124
+ if (!publicKey) return false
125
+ try {
126
+ const pem = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`
127
+ const verify = crypto.createVerify('RSA-SHA1')
128
+ verify.update(signStr, 'utf8')
129
+ return verify.verify(pem, sign, 'base64')
130
+ } catch (e) {
131
+ return false
132
+ }
133
+ }
134
+
85
135
  const check = ({ timestamp, nonce, signature, token }) => {
86
136
  const tmp = [token, timestamp, nonce].sort().join('')
87
137
  const currSign = crypto.createHash('sha1').update(tmp).digest('hex')
@@ -1453,11 +1503,74 @@ exports.union_notify = async (ctx) => {
1453
1503
  try {
1454
1504
  const notifyData = ctx.request.body
1455
1505
 
1506
+ // TODO: 临时观察日志,上线后删除
1507
+ app.service.log.push({
1508
+ app,
1509
+ message: `通联融合回调union_notify报文: ${JSON.stringify(notifyData || {})}`,
1510
+ })
1511
+
1512
+ if (!notifyData || typeof notifyData !== 'object') {
1513
+ ctx.status = 400
1514
+ ctx.body = 'FAIL'
1515
+ return
1516
+ }
1517
+
1518
+ // ===== 通联(AllinPay)回调验签 + 商户号绑定 =====
1519
+ const pay_config = 'weixin_pay'
1520
+ const appConfig = getConfig(app)
1521
+ const {
1522
+ union_mchId,
1523
+ union_appid,
1524
+ union_publicKey,
1525
+ union_md5Key,
1526
+ union_signType,
1527
+ } = await appConfig.getObject(pay_config)
1528
+ const cusid = notifyData.cusid
1529
+
1530
+ if (cusid && union_mchId && cusid !== union_mchId) {
1531
+ app.service.log.push({
1532
+ app,
1533
+ message: `通联回调商户号不匹配: cfg=${union_mchId} got=${cusid}`,
1534
+ })
1535
+ ctx.body = 'FAIL'
1536
+ return
1537
+ }
1538
+
1539
+ // 通联回调报文字段为全小写 signtype;兼容大小写两种 + 配置 union_signType;末位回退 RSA
1540
+ const rawSignType = (notifyData.signtype || notifyData.signType || union_signType || 'RSA').toUpperCase()
1541
+ const hasSign = notifyData.sign !== undefined && notifyData.sign !== ''
1542
+ const trxstatus = notifyData.trxstatus
1543
+
1544
+ // 仅当成功状态(trxstatus==='0000')强制要求签名;失败状态没有 sign 时仍打日志但直接返回 success(不重放)
1545
+ if (trxstatus === '0000' && !hasSign) {
1546
+ app.service.log.push({
1547
+ app,
1548
+ message: `通联回调签名缺失: ${JSON.stringify(notifyData)}`,
1549
+ })
1550
+ ctx.body = 'FAIL'
1551
+ return
1552
+ }
1553
+
1554
+ if (hasSign) {
1555
+ const signOk = validateUnionNotifySign({
1556
+ notifyData,
1557
+ signType: rawSignType,
1558
+ publicKey: union_publicKey,
1559
+ md5Key: union_md5Key,
1560
+ })
1561
+ if (!signOk) {
1562
+ app.service.log.push({
1563
+ app,
1564
+ message: `通联回调验签失败(type=${rawSignType}): ${JSON.stringify(notifyData)}`,
1565
+ })
1566
+ ctx.body = 'FAIL'
1567
+ return
1568
+ }
1569
+ }
1570
+
1456
1571
  const {
1457
1572
  trxcode,
1458
- trxstatus,
1459
1573
  cusorderid,
1460
- cusid,
1461
1574
  acct,
1462
1575
  trxid,
1463
1576
  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.34",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {