feat: 微信信息验证 & echo功能
This commit is contained in:
147
package-lock.json
generated
147
package-lock.json
generated
@ -7,6 +7,10 @@
|
||||
"": {
|
||||
"name": "directus-extension-wechat-service",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "^5.3.0",
|
||||
"raw-body": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "16.0.2",
|
||||
"@types/node": "^24.9.1",
|
||||
@ -1787,7 +1791,6 @@
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@ -1872,6 +1875,7 @@
|
||||
"integrity": "sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@unhead/schema": "1.11.20",
|
||||
"@unhead/shared": "1.11.20"
|
||||
@ -1886,6 +1890,7 @@
|
||||
"integrity": "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"hookable": "^5.5.3",
|
||||
"zhead": "^2.2.4"
|
||||
@ -1900,6 +1905,7 @@
|
||||
"integrity": "sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@unhead/schema": "1.11.20",
|
||||
"packrup": "^0.1.2"
|
||||
@ -2004,7 +2010,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.18",
|
||||
@ -2182,7 +2189,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@ -2204,6 +2210,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@ -2691,6 +2706,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@ -2865,7 +2889,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@ -2962,6 +2985,24 @@
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.0.tgz",
|
||||
"integrity": "sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@ -3266,7 +3307,24 @@
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "8.0.1",
|
||||
@ -3282,7 +3340,6 @@
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@ -3335,6 +3392,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "12.9.0",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.0.tgz",
|
||||
@ -3546,7 +3609,6 @@
|
||||
"integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"colorette": "2.0.19",
|
||||
"commander": "^10.0.0",
|
||||
@ -3919,6 +3981,7 @@
|
||||
"integrity": "sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/harlan-zw"
|
||||
}
|
||||
@ -4035,6 +4098,7 @@
|
||||
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
@ -4059,6 +4123,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
@ -4099,7 +4164,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -4701,6 +4765,21 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
||||
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.7.0",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
|
||||
@ -4788,7 +4867,6 @@
|
||||
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -4946,7 +5024,6 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
@ -4979,6 +5056,12 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@ -5066,6 +5149,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
@ -5126,6 +5218,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
|
||||
"integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz",
|
||||
@ -5255,6 +5359,15 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@ -5281,7 +5394,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -5303,6 +5415,7 @@
|
||||
"integrity": "sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@unhead/dom": "1.11.20",
|
||||
"@unhead/schema": "1.11.20",
|
||||
@ -5336,6 +5449,15 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-utils": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz",
|
||||
@ -5397,7 +5519,6 @@
|
||||
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -5473,7 +5594,6 @@
|
||||
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.18",
|
||||
"@vue/compiler-sfc": "3.5.18",
|
||||
@ -5598,6 +5718,7 @@
|
||||
"integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/harlan-zw"
|
||||
}
|
||||
|
||||
@ -28,5 +28,9 @@
|
||||
"@directus/extensions-sdk": "16.0.2",
|
||||
"@types/node": "^24.9.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "^5.3.0",
|
||||
"raw-body": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
75
src/index.ts
75
src/index.ts
@ -1,8 +1,79 @@
|
||||
import { defineEndpoint } from "@directus/extensions-sdk";
|
||||
import { parseStringXML, parseWechatEncrypt, parseWechatMessage } from "./utils/xml-parser";
|
||||
import { verifyEncrypt, verifySignature } from "./utils/verification";
|
||||
import crypto from 'crypto'
|
||||
import { WechatCrypto } from "./utils/wechatCrypto";
|
||||
|
||||
export default defineEndpoint({
|
||||
id: "wechat-service",
|
||||
handler: (router) => {
|
||||
router.get("/", (_req, res) => res.send("Hello, Wechat!"));
|
||||
handler: (router, { env }) => {
|
||||
router.get('/', async (req, res) => {
|
||||
const { signature, timestamp, nonce, echostr } = req.query;
|
||||
|
||||
const token = env.WECHAT_TOKEN;
|
||||
|
||||
if (verifySignature(token as string, signature as string, timestamp as string, nonce as string)) {
|
||||
return res.send(echostr);
|
||||
}
|
||||
return res.status(403).send('Invalid signature');
|
||||
});
|
||||
|
||||
router.post("/echo", async (req, res) => {
|
||||
const jsonData = await parseWechatMessage(req);
|
||||
return res.json({ received: jsonData });
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
// 验证Encrypt签名
|
||||
const { timestamp, nonce, msg_signature } = req.query;
|
||||
const token = env.WECHAT_TOKEN;
|
||||
|
||||
const encryptData = await parseWechatEncrypt(req);
|
||||
const encrypt = encryptData.Encrypt;
|
||||
|
||||
if (!verifyEncrypt(token as string, encrypt as string, timestamp as string, nonce as string, msg_signature as string)) {
|
||||
console.error("Invalid Signature");
|
||||
return res.status(403).send('Invalid Signature');
|
||||
}
|
||||
|
||||
// 处理加密信息体
|
||||
const encodingAESKey = env.WECHAT_AESKEY;
|
||||
const appId = env.WECHAT_APPID;
|
||||
const cryptoUtil = new WechatCrypto(encodingAESKey, appId);
|
||||
const decryptXml = cryptoUtil.decrypt(encrypt);
|
||||
const msg = await parseStringXML(decryptXml);
|
||||
let replyContent;
|
||||
if (msg.MsgType !== 'text') {
|
||||
replyContent = "暂不支持该消息类型";
|
||||
} else {
|
||||
replyContent = `已收到你的加密消息:${msg.Content}`;
|
||||
|
||||
}
|
||||
|
||||
// 回复消息(示例:文本)
|
||||
const reply = `<xml>
|
||||
<ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
|
||||
<FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
|
||||
<CreateTime>${Date.now()}</CreateTime>
|
||||
<MsgType><![CDATA[text]]></MsgType>
|
||||
<Content><![CDATA[${replyContent}]]></Content>
|
||||
</xml>`;
|
||||
|
||||
const encryptedReply = cryptoUtil.encrypt(reply);
|
||||
|
||||
// 再次签名
|
||||
const replyArr = [token, timestamp, nonce, encryptedReply].sort();
|
||||
const replySig = crypto.createHash('sha1').update(replyArr.join('')).digest('hex');
|
||||
|
||||
const responseXml = `<xml>
|
||||
<Encrypt><![CDATA[${encryptedReply}]]></Encrypt>
|
||||
<MsgSignature><![CDATA[${replySig}]]></MsgSignature>
|
||||
<TimeStamp>${timestamp}</TimeStamp>
|
||||
<Nonce><![CDATA[${nonce}]]></Nonce>
|
||||
</xml>`;
|
||||
|
||||
res.set('Content-Type', 'application/xml');
|
||||
return res.send(responseXml);
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
30
src/types/wechat-message.ts
Normal file
30
src/types/wechat-message.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export interface WechatEncryptMessage {
|
||||
ToUserName: string;
|
||||
Encrypt: string;
|
||||
}
|
||||
|
||||
export interface WechatBaseMessage {
|
||||
/** 接收方微信号 */
|
||||
ToUserName: string;
|
||||
/** 发送方微信号 */
|
||||
FromUserName: string;
|
||||
/** 消息创建时间 */
|
||||
CreateTime: number;
|
||||
/** 消息类型 */
|
||||
MsgType: string;
|
||||
}
|
||||
|
||||
export interface WechatTextMessage extends WechatBaseMessage {
|
||||
/** 文本消息类型 */
|
||||
MsgType: "text";
|
||||
/** 文本消息内容 */
|
||||
Content: string;
|
||||
/** 消息id,64位整型 */
|
||||
MsgId: string;
|
||||
/** 消息数据id,消息来自文章时存在 */
|
||||
MsgDataId?: string;
|
||||
/** 多图文时的文章索引,从1开始,消息来自文章时存在 */
|
||||
Idx?: string;
|
||||
}
|
||||
|
||||
export type WechatMessage = WechatTextMessage;
|
||||
17
src/utils/verification.ts
Normal file
17
src/utils/verification.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function verifySignature(token: string, signature: string, timestamp: string, nonce: string): boolean {
|
||||
const tempArray = [token, timestamp, nonce].sort();
|
||||
const str = tempArray.join('');
|
||||
const sha1 = crypto.createHash('sha1');
|
||||
const hash = sha1.update(str).digest('hex');
|
||||
return hash === signature
|
||||
}
|
||||
|
||||
export function verifyEncrypt(token: string, encrypt: string, timestamp: string, nonce: string, msg_signature: string): boolean {
|
||||
const tempArray = [token, timestamp, nonce, encrypt].sort();
|
||||
const str = tempArray.join('');
|
||||
const sha1 = crypto.createHash('sha1');
|
||||
const hash = sha1.update(str).digest('hex');
|
||||
return hash === msg_signature
|
||||
}
|
||||
59
src/utils/wechatCrypto.ts
Normal file
59
src/utils/wechatCrypto.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* 微信AES加解密工具
|
||||
*/
|
||||
export class WechatCrypto {
|
||||
AESKey: Buffer;
|
||||
iv: Buffer;
|
||||
AppId: string;
|
||||
|
||||
constructor(encodingAESKey: string, appId: string) {
|
||||
this.AESKey = Buffer.from(encodingAESKey + '=', 'base64');
|
||||
this.iv = this.AESKey.subarray(0, 16);
|
||||
this.AppId = appId;
|
||||
}
|
||||
|
||||
/** 信息体解密 */
|
||||
decrypt(encrypt: string) {
|
||||
const TmpMsg = Buffer.from(encrypt, 'base64');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', this.AESKey, this.iv);
|
||||
decipher.setAutoPadding(false);
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(TmpMsg),
|
||||
decipher.final(),
|
||||
]);
|
||||
|
||||
let pad = decrypted[decrypted.length - 1]!;
|
||||
if (pad < 1 || pad > 32) pad = 0;
|
||||
const content = decrypted.subarray(16, decrypted.length - pad);
|
||||
const msgLength = content.readUInt32BE(0);
|
||||
const msg = content.subarray(4, 4 + msgLength).toString('utf-8');
|
||||
const appId = content.subarray(4 + msgLength).toString('utf-8');
|
||||
|
||||
if (appId !== this.AppId) {
|
||||
throw new Error('AppID mismatch');
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** 信息体加密 */
|
||||
encrypt(replyMsg: string) {
|
||||
const random16 = crypto.randomBytes(16);
|
||||
const msg = Buffer.from(replyMsg);
|
||||
const msgLength = Buffer.alloc(4);
|
||||
msgLength.writeUInt32BE(msg.length, 0);
|
||||
const appIdBuffer = Buffer.from(this.AppId);
|
||||
|
||||
const raw = Buffer.concat([random16, msgLength, msg, appIdBuffer]);
|
||||
const padLen = 32 - (raw.length % 32);
|
||||
const pad = Buffer.alloc(padLen, padLen);
|
||||
const content = Buffer.concat([raw, pad]);
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', this.AESKey, this.iv);
|
||||
cipher.setAutoPadding(false);
|
||||
const encrypted = Buffer.concat([cipher.update(content), cipher.final()]);
|
||||
return encrypted.toString('base64');
|
||||
}
|
||||
}
|
||||
103
src/utils/xml-parser.ts
Normal file
103
src/utils/xml-parser.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import getRawBody from "raw-body";
|
||||
import type { Request, Response } from "express";
|
||||
import { WechatEncryptMessage, WechatMessage } from "../types/wechat-message";
|
||||
|
||||
export async function parseWechatEncrypt(req: Request): Promise<WechatEncryptMessage> {
|
||||
const raw = await getRawBody(req, { encoding: "utf-8" });
|
||||
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
cdataPropName: '__cdata'
|
||||
})
|
||||
const result = parser.parse(raw.toString());
|
||||
|
||||
const xml = result.xml as Record<string, unknown>;
|
||||
|
||||
// 将可能存在的 __cdata 提取
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const key in xml) {
|
||||
const value = xml[key];
|
||||
if (typeof value === 'object' && value && '__cdata' in value) {
|
||||
normalized[key] = (value as { __cdata: string }).__cdata;
|
||||
} else {
|
||||
normalized[key] = value as string;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ToUserName: normalized.ToUserName,
|
||||
...normalized,
|
||||
} as WechatEncryptMessage;
|
||||
}
|
||||
|
||||
export async function parseStringXML(raw: string): Promise<WechatMessage> {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
cdataPropName: '__cdata',
|
||||
});
|
||||
const result = parser.parse(raw.toString());
|
||||
|
||||
const xml = result.xml as Record<string, unknown>;
|
||||
|
||||
console.log("Parsed XML:", xml);
|
||||
|
||||
// 将可能存在的 __cdata 提取
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const key in xml) {
|
||||
const value = xml[key];
|
||||
if (typeof value === 'object' && value && '__cdata' in value) {
|
||||
normalized[key] = (value as { __cdata: string }).__cdata;
|
||||
} else {
|
||||
normalized[key] = value as string;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(normalized.CreateTime)
|
||||
|
||||
return {
|
||||
ToUserName: normalized.ToUserName,
|
||||
FromUserName: normalized.FromUserName,
|
||||
CreateTime: Number(normalized.CreateTime),
|
||||
MsgType: normalized.MsgType,
|
||||
...normalized,
|
||||
} as WechatMessage;
|
||||
}
|
||||
|
||||
export async function parseWechatMessage(
|
||||
req: Request,
|
||||
): Promise<WechatMessage> {
|
||||
const raw = await getRawBody(req, { encoding: "utf-8" });
|
||||
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
cdataPropName: '__cdata',
|
||||
});
|
||||
const result = parser.parse(raw.toString());
|
||||
|
||||
const xml = result.xml as Record<string, unknown>;
|
||||
|
||||
console.log("Parsed XML:", xml);
|
||||
|
||||
// 将可能存在的 __cdata 提取
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const key in xml) {
|
||||
const value = xml[key];
|
||||
if (typeof value === 'object' && value && '__cdata' in value) {
|
||||
normalized[key] = (value as { __cdata: string }).__cdata;
|
||||
} else {
|
||||
normalized[key] = value as string;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(normalized.CreateTime)
|
||||
|
||||
return {
|
||||
ToUserName: normalized.ToUserName,
|
||||
FromUserName: normalized.FromUserName,
|
||||
CreateTime: Number(normalized.CreateTime),
|
||||
MsgType: normalized.MsgType,
|
||||
...normalized,
|
||||
} as WechatMessage;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user