feat: 微信信息验证 & echo功能

This commit is contained in:
2025-10-31 05:04:38 +00:00
parent ef07c35825
commit d8074ea6d9
7 changed files with 420 additions and 15 deletions

View File

@ -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);
})
},
});

View 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;
/** 消息id64位整型 */
MsgId: string;
/** 消息数据id,消息来自文章时存在 */
MsgDataId?: string;
/** 多图文时的文章索引从1开始消息来自文章时存在 */
Idx?: string;
}
export type WechatMessage = WechatTextMessage;

17
src/utils/verification.ts Normal file
View 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
View 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
View 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;
}