webhook-verify.js 2.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. const crypto = require('crypto');
  2. const { readConfig } = require('./webhook-config');
  3. function getHeader(headers, name) {
  4. const lower = name.toLowerCase();
  5. for (const [key, value] of Object.entries(headers)) {
  6. if (key.toLowerCase() === lower) {
  7. if (Array.isArray(value)) return value[0];
  8. return value;
  9. }
  10. }
  11. return undefined;
  12. }
  13. function extractSignature(headers) {
  14. const auth = getHeader(headers, 'authorization');
  15. if (auth) {
  16. const bearerMatch = auth.match(/^Bearer\s+(.+)$/i);
  17. if (bearerMatch) return bearerMatch[1].trim();
  18. return auth.trim();
  19. }
  20. const sigHeaders = ['x-qiwe-signature', 'x-qiweapi-signature', 'x-signature'];
  21. for (const name of sigHeaders) {
  22. const val = getHeader(headers, name);
  23. if (val) return val;
  24. }
  25. return null;
  26. }
  27. function timingSafeEqualStrings(a, b) {
  28. if (a.length !== b.length) return false;
  29. return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
  30. }
  31. function verifyWebhookSignature(rawBody, headers) {
  32. const config = readConfig();
  33. if (process.env.QIWEI_WEBHOOK_ALLOW_UNSECURED === 'true') {
  34. console.warn('[Webhook] QIWEI_WEBHOOK_ALLOW_UNSECURED=true,允许未签名回调(仅开发环境使用)');
  35. return true;
  36. }
  37. if (!config.secret) {
  38. console.warn('[Webhook] Webhook 未配置 secret,回调已拒绝');
  39. return false;
  40. }
  41. const signature = extractSignature(headers);
  42. if (!signature) {
  43. console.warn('[Webhook] 请求缺少签名信息');
  44. return false;
  45. }
  46. if (timingSafeEqualStrings(signature, config.secret)) {
  47. return true;
  48. }
  49. try {
  50. const expected = crypto
  51. .createHmac('sha256', config.secret)
  52. .update(rawBody, 'utf8')
  53. .digest('hex');
  54. if (timingSafeEqualStrings(expected, signature)) {
  55. return true;
  56. }
  57. } catch {
  58. // ignore
  59. }
  60. console.warn('[Webhook] 签名验证失败');
  61. return false;
  62. }
  63. module.exports = {
  64. verifyWebhookSignature,
  65. extractSignature,
  66. getHeader,
  67. timingSafeEqualStrings
  68. };