微信支付 域名授权 多个域名 - 完整解决方案与实战教程

在微信生态开发中,我们经常遇到这样的业务场景:一个商户号(MchID)需要同时支持多个域名下的支付发起,例如主站、活动子站、H5商城、小程序WebView、甚至不同地区的独立域名。很多团队在测试环境只配置了一个域名,上线后突然发现“当前页面的URL未注册”或“商户号该产品权限未开通”等报错。核心痛点在于:微信支付的JSAPI支付、H5支付、Native支付对“授权目录”和“支付授权域名”的校验是严格且多层次的,而微信商户平台默认只允许配置少量域名,且不支持通配符泛解析,导致多域名场景下频繁掉坑。本文将基于真实项目经验,从现象、原因到完整代码方案,帮你彻底解决“微信支付 域名授权 多个域名”的难题。

问题现象

开发者通常会在调用微信支付时遇到以下典型错误:

原因分析

微信支付的域名授权机制并非单一维度,而是分为支付授权目录H5支付域名JSAPI支付授权目录Native支付回调URL等多个独立配置项。常见坑点如下:

  1. 配置位置错误:JSAPI支付授权目录在“商户平台-产品中心-JSAPI支付-支付授权目录”中配置,而H5支付域名在“H5支付-支付域名”中配置,两者不互通。
  2. 目录格式要求严格:JSAPI授权目录必须以/结尾,且必须精确到子目录,例如https://pay.example.com/wxpay/,不能是https://pay.example.com
  3. 域名数量限制:微信商户平台对每个支付产品通常只允许配置5个授权目录或域名,超过后无法添加。
  4. 不支持通配符:无法使用*.example.com,每个子域名必须单独配置。
  5. 跨域与Referer丢失:在H5支付中,如果页面跳转导致Referer丢失或跨域,微信会拒绝支付。
  6. 缓存与生效时间:配置修改后通常需要5-10分钟生效,部分开发者立即测试导致误判。

解决方案(附完整代码)

针对多域名场景,推荐采用“统一支付网关 + 动态授权目录 + 后端代理”的架构。核心思路:将所有域名的支付请求统一转发到同一个已授权的支付域名下,或者通过后端动态生成支付参数时,根据当前域名动态调整redirect_urlreferer。以下以Node.js(Express)为例,展示如何实现多域名兼容的微信JSAPI支付。

步骤一:商户平台配置优化

步骤二:后端动态生成支付参数(Node.js)

以下代码演示如何根据请求的OriginReferer动态设置redirect_url,并生成JSAPI支付参数。注意:redirect_url必须与当前页面同域,否则微信会校验失败。

const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const app = express();
app.use(express.json());

// 微信支付配置(从环境变量读取)
const WX_CONFIG = {
  appId: process.env.WX_APPID,
  mchId: process.env.WX_MCHID,
  apiKey: process.env.WX_APIKEY, // APIv3密钥
  serialNo: process.env.WX_SERIAL_NO, // 证书序列号
  privateKey: process.env.WX_PRIVATE_KEY // 商户私钥
};

// 允许的域名白名单(与商户平台配置保持一致)
const ALLOWED_ORIGINS = [
  'https://www.example.com',
  'https://m.example.com',
  'https://activity.example.com',
  'https://pay.example.com'
];

// 生成随机字符串
function generateNonceStr() {
  return crypto.randomBytes(16).toString('hex');
}

// 生成签名(APIv3)
function generateSignature(method, url, timestamp, nonceStr, body) {
  const message = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`;
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(message);
  return sign.sign(WX_CONFIG.privateKey, 'base64');
}

// 核心接口:获取JSAPI支付参数
app.post('/api/wxpay/jsapi', async (req, res) => {
  try {
    const { openId, totalFee, outTradeNo, description } = req.body;
    const origin = req.headers.origin || req.headers.referer;

    // 1. 校验来源域名是否在白名单内
    const isAllowed = ALLOWED_ORIGINS.some(allowed => origin && origin.startsWith(allowed));
    if (!isAllowed) {
      return res.status(403).json({ error: '域名未授权,请检查商户平台配置' });
    }

    // 2. 动态设置 redirect_url(必须与当前页面同域)
    // 注意:微信要求 redirect_url 的域名必须与支付授权目录匹配
    const redirectUrl = `${origin}/pay/success`;

    // 3. 构造统一下单请求(APIv3)
    const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi';
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonceStr = generateNonceStr();
    const body = JSON.stringify({
      appid: WX_CONFIG.appId,
      mchid: WX_CONFIG.mchId,
      description: description,
      out_trade_no: outTradeNo,
      notify_url: 'https://pay.example.com/api/wxpay/notify', // 统一回调地址
      amount: { total: totalFee, currency: 'CNY' },
      payer: { openid: openId },
      scene_info: {
        payer_client_ip: req.ip,
        h5_info: { type: 'Wap' }
      },
      redirect_url: redirectUrl // 关键:动态设置,兼容多域名
    });

    const signature = generateSignature('POST', '/v3/pay/transactions/jsapi', timestamp, nonceStr, body);
    const authHeader = `WECHATPAY2-SHA256-RSA2048 mchid="${WX_CONFIG.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${WX_CONFIG.serialNo}"`;

    const response = await axios.post(url, body, {
      headers: {
        'Authorization': authHeader,
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }
    });

    const prepayId = response.data.prepay_id;

    // 4. 生成前端调起支付所需的参数
    const payTimestamp = Math.floor(Date.now() / 1000).toString();
    const payNonceStr = generateNonceStr();
    const payPackage = `prepay_id=${prepayId}`;
    const paySignMessage = `${WX_CONFIG.appId}\n${payTimestamp}\n${payNonceStr}\n${payPackage}\n`;
    const paySign = crypto.createSign('RSA-SHA256').update(paySignMessage).sign(WX_CONFIG.privateKey, 'base64');

    res.json({
      appId: WX_CONFIG.appId,
      timeStamp: payTimestamp,
      nonceStr: payNonceStr,
      package: payPackage,
      signType: 'RSA',
      paySign: paySign
    });

  } catch (error) {
    console.error('微信支付下单失败:', error.response ? error.response.data : error.message);
    res.status(500).json({ error: '支付下单失败', detail: error.message });
  }
});

// 统一支付回调(所有域名共用)
app.post('/api/wxpay/notify', (req, res) => {
  // 验签、解密、处理业务逻辑
  // 注意:回调地址必须为HTTPS且公网可访问
  console.log('收到支付回调:', req.body);
  res.json({ code: 'SUCCESS', message: '成功' });
});

app.listen(3000, () => {
  console.log('支付网关运行在3000端口');
});

步骤三:前端多域名适配

前端在调用支付时,需要确保当前页面的URL与商户平台配置的授权目录完全匹配。如果使用Vue/React等SPA,注意history模式下的路径问题。

// 前端调用示例(微信JSAPI)
async function requestWxPay(orderInfo) {
  // 1. 从后端获取支付参数
  const res = await fetch('/api/wxpay/jsapi', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      openId: orderInfo.openId,
      totalFee: orderInfo.totalFee,
      outTradeNo: orderInfo.outTradeNo,
      description: orderInfo.description
    })
  });

  const payParams = await res.json();

  // 2. 调起微信支付
  if (typeof WeixinJSBridge === 'undefined') {
    alert('请在微信中打开');
    return;
  }

  WeixinJSBridge.invoke('getBrandWCPayRequest', {
    appId: payParams.appId,
    timeStamp: payParams.timeStamp,
    nonceStr: payParams.nonceStr,
    package: payParams.package,
    signType: payParams.signType,
    paySign: payParams.paySign
  }, function(res) {
    if (res.err_msg === 'get_brand_wcpay_request:ok') {
      // 支付成功,跳转到订单页
      window.location.href = '/pay/success';
    } else {
      alert('支付失败: ' + res.err_msg);
    }
  });
}

步骤四:排查清单

通过以上“统一网关 + 动态redirect_url + 白名单校验”的方案,可以稳定支持多个域名下的微信支付。如果域名数量超过5个,建议将所有支付请求收敛到pay.example.com,业务域名仅做跳转,这样只需维护一个授权目录,大幅降低维护成本。