小程序微信后台和代理后台使用同一个域名
This commit is contained in:
@@ -39,20 +39,28 @@ app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// Step 1: 小程序跳转到这里,服务器重定向到微信授权页
|
||||
app.get('/auth/oa/login', (req, res) => {
|
||||
// 1. 尝试从远程配置获取动态域名
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
const key = config.officialAccount.redirectDomainKey || 'auth_redirect_domain';
|
||||
|
||||
let currentDomain = remoteConfig.getParaValue(key, agentid, gameid, channelid, marketid);
|
||||
|
||||
if (!currentDomain) {
|
||||
return res.status(500).send('Configuration Error: No redirect domain available in remote config.');
|
||||
}
|
||||
|
||||
// 3. 格式化域名 (确保有 https且无末尾斜杠)
|
||||
currentDomain = currentDomain.trim().replace(/\/$/, '');
|
||||
if (!currentDomain.startsWith('http')) {
|
||||
currentDomain = `https://${currentDomain}`;
|
||||
let currentDomain;
|
||||
|
||||
if (config.officialAccount.redirectDomain) {
|
||||
// 优先使用环境变量 WX_OA_REDIRECT_DOMAIN(支持带路径前缀,如 https://api.xxx.cn/wx)
|
||||
currentDomain = config.officialAccount.redirectDomain.trim().replace(/\/$/, '');
|
||||
if (!currentDomain.startsWith('http')) {
|
||||
currentDomain = `https://${currentDomain}`;
|
||||
}
|
||||
} else {
|
||||
// 从远程配置获取动态域名
|
||||
const { agentid, gameid, channelid, marketid } = config.remoteConfig;
|
||||
const key = config.officialAccount.redirectDomainKey || 'auth_redirect_domain';
|
||||
currentDomain = remoteConfig.getParaValue(key, agentid, gameid, channelid, marketid);
|
||||
|
||||
if (!currentDomain) {
|
||||
return res.status(500).send('Configuration Error: No redirect domain available in remote config.');
|
||||
}
|
||||
|
||||
currentDomain = currentDomain.trim().replace(/\/$/, '');
|
||||
if (!currentDomain.startsWith('http')) {
|
||||
currentDomain = `https://${currentDomain}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 这里的 redirect_uri 必须是 encodeURIComponent 后的完整 URL
|
||||
@@ -65,26 +73,48 @@ app.get('/auth/oa/login', (req, res) => {
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
// code 使用缓存:防止微信 webview 双重请求导致 40163 "code been used"
|
||||
// value 可能是:Promise(处理中)或 string(已完成的 HTML),TTL 60 秒后自动清理
|
||||
const codeCache = new Map();
|
||||
function setCacheWithTTL(code, value, ttlMs = 60000) {
|
||||
codeCache.set(code, value);
|
||||
setTimeout(() => codeCache.delete(code), ttlMs);
|
||||
}
|
||||
|
||||
// Step 2: 微信回调,获取 code -> access_token -> userinfo -> 返回 HTML 给 web-view
|
||||
app.get('/auth/oa/callback', async (req, res) => {
|
||||
const code = req.query.code;
|
||||
// console.log('WeChat Callback Code:', code); // 打印回调 Code
|
||||
logger.info('Auth', 'WeChat Callback Code:', code);
|
||||
|
||||
if (!code) return res.send('Auth failed, no code');
|
||||
|
||||
// 命中缓存:等待相同 code 的进行中 Promise 或直接返回已缓存 HTML
|
||||
// 防止微信 webview 双重请求(race condition)触发 40163
|
||||
if (codeCache.has(code)) {
|
||||
logger.info('Auth', 'Returning cached response for code:', code);
|
||||
const cached = codeCache.get(code);
|
||||
const html = (cached instanceof Promise) ? await cached : cached;
|
||||
return res.send(html);
|
||||
}
|
||||
|
||||
// 立即写入 Promise 占位,后续并发请求直接等待此 Promise
|
||||
let resolveHtml;
|
||||
const htmlPromise = new Promise(resolve => { resolveHtml = resolve; });
|
||||
setCacheWithTTL(code, htmlPromise);
|
||||
|
||||
try {
|
||||
// 1. 获取 access_token
|
||||
const tokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.officialAccount.appId}&secret=${config.officialAccount.appSecret}&code=${code}&grant_type=authorization_code`;
|
||||
// console.log('Requesting Token URL:', tokenUrl); // 打印 Token 请求链接 (注意:生产环境不要打印 Secret)
|
||||
logger.debug('Auth', 'Requesting Token URL (Secret Hidden)');
|
||||
|
||||
const tokenRes = await axios.get(tokenUrl);
|
||||
// console.log('Token Response:', tokenRes.data); // 打印 Token 响应结果
|
||||
logger.debug('Auth', 'Token Response:', tokenRes.data);
|
||||
|
||||
if (tokenRes.data.errcode) {
|
||||
return res.send('Token Error: ' + JSON.stringify(tokenRes.data));
|
||||
const errHtml = 'Token Error: ' + JSON.stringify(tokenRes.data);
|
||||
resolveHtml(errHtml);
|
||||
setCacheWithTTL(code, errHtml);
|
||||
return res.send(errHtml);
|
||||
}
|
||||
|
||||
const { access_token, openid } = tokenRes.data;
|
||||
@@ -92,50 +122,65 @@ app.get('/auth/oa/callback', async (req, res) => {
|
||||
// 2. 获取用户信息 (包含永久头像 headimgurl)
|
||||
const infoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`;
|
||||
const infoRes = await axios.get(infoUrl);
|
||||
// console.log('User Info Response:', infoRes.data); // 打印用户信息响应
|
||||
logger.success('Auth', 'User Info Retrieved:', infoRes.data.nickname);
|
||||
|
||||
const userInfo = infoRes.data; // { headimgurl, nickname, unionid, ... }
|
||||
|
||||
// 3. 返回一个 HTML,利用 JSSDK 把数据传回小程序
|
||||
// 注意:wx.miniProgram.postMessage 只有在页面后退、销毁、分享时才会触发小程序的 bindmessage
|
||||
// 所以这里我们发送数据后,立即调用 navigateBack
|
||||
// 注意:wx.miniProgram.postMessage 只有在页面后退、销毁时才会触发小程序的 bindmessage
|
||||
// 重要:JSON 中的 < > & 必须转义为 unicode 转义序列,避免昵称/URL 中含 </script> 导致
|
||||
// HTML 解析器提前关闭 script 标签,使整段 JS 失效(表现为一直循环无法返回小程序)
|
||||
const safeUserData = JSON.stringify(userInfo)
|
||||
.replace(/</g, '\\u003c')
|
||||
.replace(/>/g, '\\u003e')
|
||||
.replace(/&/g, '\\u0026');
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>正在同步...</title>
|
||||
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h3 style="text-align:center; margin-top: 50px;">正在同步微信头像...</h3>
|
||||
<script>
|
||||
function sendAndBack() {
|
||||
// 发送消息给小程序
|
||||
wx.miniProgram.postMessage({
|
||||
data: ${JSON.stringify(userInfo)}
|
||||
});
|
||||
// 跳回小程序上一页
|
||||
wx.miniProgram.navigateBack();
|
||||
var userData = ${safeUserData};
|
||||
var _called = false;
|
||||
|
||||
function doNavigate() {
|
||||
if (_called) return;
|
||||
_called = true;
|
||||
try { wx.miniProgram.postMessage({ data: userData }); } catch(e) {}
|
||||
try {
|
||||
wx.miniProgram.navigateBack({ delta: 1 });
|
||||
} catch(e) {
|
||||
try { wx.miniProgram.switchTab({ url: '/pages/profile/profile' }); } catch(e2) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (window.WeixinJSBridge) {
|
||||
sendAndBack();
|
||||
if (typeof WeixinJSBridge !== 'undefined') {
|
||||
doNavigate();
|
||||
} else {
|
||||
document.addEventListener('WeixinJSBridgeReady', sendAndBack, false);
|
||||
document.addEventListener('WeixinJSBridgeReady', doNavigate, false);
|
||||
}
|
||||
// 保底策略
|
||||
setTimeout(sendAndBack, 1000);
|
||||
setTimeout(doNavigate, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// 将最终 HTML 字符串写入缓存(替换 Promise 占位)
|
||||
setCacheWithTTL(code, html);
|
||||
resolveHtml(html);
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.send('Auth error: ' + error.message);
|
||||
const errHtml = 'Auth error: ' + error.message;
|
||||
resolveHtml(errHtml);
|
||||
res.send(errHtml);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user