feat: 企微回调服务基建(加解密+事件分发,默认关闭)
This commit is contained in:
125
app/api/wecom-callback/route.ts
Normal file
125
app/api/wecom-callback/route.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
decryptWecomCallback,
|
||||
verifyWecomCallbackSignature,
|
||||
wecomSignaturesMatch,
|
||||
WecomCallbackCryptoError,
|
||||
} from "../../../lib/wecom-callback-crypto";
|
||||
import {
|
||||
extractWecomEncryptField,
|
||||
handleWecomCallbackEvent,
|
||||
parseWecomCallbackXml,
|
||||
} from "../../../lib/wecom-callback-events";
|
||||
import { isEnabled, getRuntimeEnv } from "../../../lib/runtime-env";
|
||||
|
||||
type CallbackConfig = {
|
||||
token: string;
|
||||
encodingAesKey: string;
|
||||
corpId: string;
|
||||
};
|
||||
|
||||
function resolveCallbackConfig(): CallbackConfig | null {
|
||||
const env = getRuntimeEnv();
|
||||
if (!isEnabled(env.WECOM_CALLBACK_ENABLED, false)) return null;
|
||||
const token = String(env.WECOM_CALLBACK_TOKEN ?? "").trim();
|
||||
const encodingAesKey = String(
|
||||
env.WECOM_CALLBACK_ENCODING_AES_KEY ?? "",
|
||||
).trim();
|
||||
if (!token || !encodingAesKey) return null;
|
||||
return {
|
||||
token,
|
||||
encodingAesKey,
|
||||
corpId: String(env.WECOM_CORP_ID ?? "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function readCallbackQuery(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
return {
|
||||
msgSignature: url.searchParams.get("msg_signature") ?? "",
|
||||
timestamp: url.searchParams.get("timestamp") ?? "",
|
||||
nonce: url.searchParams.get("nonce") ?? "",
|
||||
echostr: url.searchParams.get("echostr") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const config = resolveCallbackConfig();
|
||||
if (!config) {
|
||||
return new Response("企业微信回调未启用", { status: 403 });
|
||||
}
|
||||
const { msgSignature, timestamp, nonce, echostr } =
|
||||
readCallbackQuery(request);
|
||||
if (!msgSignature || !timestamp || !nonce || !echostr) {
|
||||
return new Response("缺少回调校验参数", { status: 400 });
|
||||
}
|
||||
const expected = verifyWecomCallbackSignature(
|
||||
config.token,
|
||||
timestamp,
|
||||
nonce,
|
||||
echostr,
|
||||
);
|
||||
if (!wecomSignaturesMatch(expected, msgSignature)) {
|
||||
return new Response("签名校验失败", { status: 403 });
|
||||
}
|
||||
try {
|
||||
const { message } = decryptWecomCallback(echostr, config.encodingAesKey);
|
||||
return new Response(message, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof WecomCallbackCryptoError) {
|
||||
console.warn("[KOC LOOP] wecom callback echostr decrypt failed", error);
|
||||
return new Response(error.message, { status: 400 });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const config = resolveCallbackConfig();
|
||||
if (!config) {
|
||||
return new Response("企业微信回调未启用", { status: 403 });
|
||||
}
|
||||
const { msgSignature, timestamp, nonce } = readCallbackQuery(request);
|
||||
try {
|
||||
const body = await request.text();
|
||||
const encrypt = extractWecomEncryptField(body);
|
||||
if (!encrypt) {
|
||||
return new Response("回调报文缺少 Encrypt 字段", { status: 400 });
|
||||
}
|
||||
if (!msgSignature || !timestamp || !nonce) {
|
||||
return new Response("缺少回调校验参数", { status: 400 });
|
||||
}
|
||||
const expected = verifyWecomCallbackSignature(
|
||||
config.token,
|
||||
timestamp,
|
||||
nonce,
|
||||
encrypt,
|
||||
);
|
||||
if (!wecomSignaturesMatch(expected, msgSignature)) {
|
||||
return new Response("签名校验失败", { status: 403 });
|
||||
}
|
||||
const { message, receiveId } = decryptWecomCallback(
|
||||
encrypt,
|
||||
config.encodingAesKey,
|
||||
);
|
||||
if (config.corpId && receiveId !== config.corpId) {
|
||||
console.warn(
|
||||
"[KOC LOOP] wecom callback receiveid mismatch",
|
||||
{ expected: config.corpId },
|
||||
);
|
||||
return new Response("receiveid 校验失败", { status: 403 });
|
||||
}
|
||||
const fields = parseWecomCallbackXml(message);
|
||||
handleWecomCallbackEvent(fields);
|
||||
return new Response("", { status: 200 });
|
||||
} catch (error) {
|
||||
if (error instanceof WecomCallbackCryptoError) {
|
||||
console.warn("[KOC LOOP] wecom callback decrypt failed", error);
|
||||
return new Response(error.message, { status: 400 });
|
||||
}
|
||||
console.error("[KOC LOOP] wecom callback failed", error);
|
||||
return new Response("", { status: 200 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user