微信支付 域名授权 多个域名 - 完整解决方案与实战教程
在微信生态开发中,我们经常遇到这样的业务场景:一个商户号(MchID)需要同时支持多个域名下的支付发起,例如主站、活动子站、H5商城、小程序WebView、甚至不同地区的独立域名。很多团队在测试环境只配置了一个域名,上线后突然发现“当前页面的URL未注册”或“商户号该产品权限未开通”等报错。核心痛点在于:微信支付的JSAPI支付、H5支付、Native支付对“授权目录”和“支付授权域名”的校验是严格且多层次的,而微信商户平台默认只允许配置少量域名,且不支持通配符泛解析,导致多域名场景下频繁掉坑。本文将基于真实项目经验,从现象、原因到完整代码方案,帮你彻底解决“微信支付 域名授权 多个域名”的难题。
问题现象
开发者通常会在调用微信支付时遇到以下典型错误:
- JSAPI支付报错:
当前页面的URL未注册,即使已经在商户平台配置了该域名。 - H5支付报错:
商家参数格式有误,请联系商家解决,或网络环境未能通过安全验证。 - Native支付扫码后提示:
商户号该产品权限未开通,请前往商户平台-产品中心-支付产品中开通,但实际已开通。 - 多个域名中,只有第一个配置的域名能支付成功,后续添加的域名随机失败。
- 使用
referer白名单时,部分域名带www能支付,不带www就失败。
原因分析
微信支付的域名授权机制并非单一维度,而是分为支付授权目录、H5支付域名、JSAPI支付授权目录、Native支付回调URL等多个独立配置项。常见坑点如下:
- 配置位置错误:JSAPI支付授权目录在“商户平台-产品中心-JSAPI支付-支付授权目录”中配置,而H5支付域名在“H5支付-支付域名”中配置,两者不互通。
- 目录格式要求严格:JSAPI授权目录必须以
/结尾,且必须精确到子目录,例如https://pay.example.com/wxpay/,不能是https://pay.example.com。 - 域名数量限制:微信商户平台对每个支付产品通常只允许配置5个授权目录或域名,超过后无法添加。
- 不支持通配符:无法使用
*.example.com,每个子域名必须单独配置。 - 跨域与Referer丢失:在H5支付中,如果页面跳转导致
Referer丢失或跨域,微信会拒绝支付。 - 缓存与生效时间:配置修改后通常需要5-10分钟生效,部分开发者立即测试导致误判。
解决方案(附完整代码)
针对多域名场景,推荐采用“统一支付网关 + 动态授权目录 + 后端代理”的架构。核心思路:将所有域名的支付请求统一转发到同一个已授权的支付域名下,或者通过后端动态生成支付参数时,根据当前域名动态调整redirect_url和referer。以下以Node.js(Express)为例,展示如何实现多域名兼容的微信JSAPI支付。
步骤一:商户平台配置优化
- 登录微信商户平台,进入“产品中心-JSAPI支付”,在“支付授权目录”中添加所有可能发起支付的完整目录(以
/结尾)。 - 如果域名超过5个,建议使用一个统一的支付中间页域名(如
pay.example.com),所有业务域名通过302跳转到该中间页发起支付。 - 在“H5支付”中配置“支付域名”,同样限制5个,建议只配置主支付域名。
- 确保所有域名均使用HTTPS,且证书有效。
步骤二:后端动态生成支付参数(Node.js)
以下代码演示如何根据请求的Origin或Referer动态设置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);
}
});
}
步骤四:排查清单
- 检查商户平台“JSAPI支付”授权目录是否包含当前页面完整URL的上级目录(以
/结尾)。 - 检查“H5支付”域名是否包含当前域名,且
redirect_url域名一致。 - 检查
Referer是否被浏览器或代理剥离,可在Nginx中配置proxy_set_header Referer $http_referer;。 - 检查APIv3密钥、证书序列号、私钥是否匹配,签名算法是否为
RSA-SHA256。 - 检查回调地址
notify_url是否公网HTTPS可访问,且无重定向。 - 配置修改后等待5-10分钟再测试,或使用微信支付提供的“验收工具”验证。
通过以上“统一网关 + 动态redirect_url + 白名单校验”的方案,可以稳定支持多个域名下的微信支付。如果域名数量超过5个,建议将所有支付请求收敛到pay.example.com,业务域名仅做跳转,这样只需维护一个授权目录,大幅降低维护成本。