feat(身份): 同源身份缓存兜底 —— H5「返回首页」裸跳不再丢 openid/unionid (v26092212)

问题(实测孕检日历,第三方应用):页面里的「返回首页」是**页面自己发起的同源跳转**,
宿主的 <web-view src> 属性没变 → _applyIdentity() 不触发 → mama-box 的 URL 上没有身份
→ 只能显示「身份不完整」;而 retryIdentity() 只是 location.replace(自己 + 缓存戳),
同一条没有身份的 URL 重载,**永远救不回来**。而且该应用会主动把 openid/phone 从自己的
URL 上擦掉(防分享链接冒名,理由见它的注释)—— 所以那一跳**不可能**带上身份。

做法(方案已与产品确认;在架 12 个应用实测全在 mp.yizhezhe.com,同源共享 storage):
- js/wx-auth.js:新增 identity()(URL 优先 → 同源 sessionStorage 兜底,命中缓存打一条
  日志便于排查)、rememberIdentity()、goHome();并在**库载入时自动采一次**身份 ——
  接入方无需改代码,只要引了本库(孕检日历就引了,且它是绝对地址 + ETag 兜底,
  我们改库即可自动生效),宿主注入过的身份就会被存下来。
- js/app.js:hashParam() 对 openid/unionid 两键加同样的缓存回落(其余参数不受影响)。

三条纪律(写在 wx-auth.js 顶部,改前先读):
① URL 永远权威:URL 带 openid 就无条件覆盖缓存(否则切号/换人会串号);
② 只在本 origin 有效:外链应用读不到,所以「跳转要带身份」的规范不能删,缓存只兜底;
③ 只存 openid/unionid(身份键),不碰 token/手机号/昵称头像。
选 sessionStorage 而非 localStorage:同一次会话内够用,且随标签页结束即失效,暴露窗口小。

验证(本地同源:/app 模拟第三方页 + /mama-box 同一 origin):
  模拟应用页自动写缓存 → 裸跳 /mama-box/ → 提示条不显示、日志出现、按缓存身份拉数据;
  回归:无缓存且无身份 → 提示条照旧显示(fail-closed 保留);URL 带身份 → 缓存被写下且不显示提示条。
This commit is contained in:
2026-09-22 21:36:52 +08:00
parent 3ab8af2bf5
commit 06f76b4df4
3 changed files with 107 additions and 7 deletions

View File

@@ -7,7 +7,7 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>兜爸心选 · 孕育陪伴</title> <title>兜爸心选 · 孕育陪伴</title>
<!-- V:26092211 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 --> <!-- V:26092212 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
<link rel="stylesheet" href="mama-box.css"> <link rel="stylesheet" href="mama-box.css">
<script 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>
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) --> <!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->

View File

@@ -3,7 +3,7 @@
(function () { (function () {
'use strict'; 'use strict';
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */ /* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
var VER = '26092211'; var VER = '26092212';
const $ = (s, c) => (c || document).querySelector(s); const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s)); const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
@@ -2748,8 +2748,23 @@
try { try {
const h = (location.hash.slice(1) || '') + '&' + (location.search.slice(1) || ''); const h = (location.hash.slice(1) || '') + '&' + (location.search.slice(1) || '');
const m = h.match(new RegExp('(?:^|[&#])' + name + '=([^&#]*)')); const m = h.match(new RegExp('(?:^|[&#])' + name + '=([^&#]*)'));
return m ? decodeURIComponent(m[1]).split('?')[0] : ''; const v = m ? decodeURIComponent(m[1]).split('?')[0] : '';
} catch (e) { return ''; } if (v) return v;
} catch (e) { /* 落到下面的兜底 */ }
/* 身份两键(openid/unionid)URL 没带时回落**同源缓存**:
H5 应用里「返回首页」是页面自己发起的同源跳转,宿主的 web-view src 没变、
_applyIdentity() 不会触发,URL 上永远不会有身份(第三方应用还会主动把
openid 从自己的 URL 上擦掉)。缓存由 wx-auth.js 的 identity() 写入 ——
凡是引了共享库的页面(含 mama-box 自己)拿到 URL 身份时都会顺手存一份。
纪律(URL 永远权威、只在本 origin 有效、只存 openid/unionid)见
js/wx-auth.js 顶部那段;命中缓存时 identity() 会打一条日志便于排查。 */
if (name === 'openid' || name === 'unionid') {
try {
const id = (window.wxAuth && window.wxAuth.identity) ? window.wxAuth.identity() : null;
if (id && id[name]) return id[name];
} catch (e) {}
}
return '';
} }
/* ---------- 身份未就绪提示(2026-09-20) ---------- /* ---------- 身份未就绪提示(2026-09-20) ----------
宿主(小程序首页壳)拿不到 openid 时会**匿名加载**本页 —— 页面照常渲染, 宿主(小程序首页壳)拿不到 openid 时会**匿名加载**本页 —— 页面照常渲染,

View File

@@ -9,6 +9,8 @@
wxAuth.isAuthorized(); // 是否已授权(手机号/昵称/头像任一非空) wxAuth.isAuthorized(); // 是否已授权(手机号/昵称/头像任一非空)
wxAuth.requestProfile(); // 点「授权登录」→ postMessage 通知宿主页 wxAuth.requestProfile(); // 点「授权登录」→ postMessage 通知宿主页
wxAuth.goMall(); // 点「进商城」→ 跳小程序商城 tabBar 页 wxAuth.goMall(); // 点「进商城」→ 跳小程序商城 tabBar 页
wxAuth.goHome(); // 点「返回首页」→ 回 mama-box(带身份;别裸写 location.href)
wxAuth.identity(); // 读 openid/unionid(URL 优先 → 同源缓存兜底)
wxAuth.watchLocation(); // 换页/路由切换自动上报当前位置 → 宿主刷新分享卡片(推荐) wxAuth.watchLocation(); // 换页/路由切换自动上报当前位置 → 宿主刷新分享卡片(推荐)
wxAuth.reportLocation({title}); // 手动上报一次当前页(标题/图可选) wxAuth.reportLocation({title}); // 手动上报一次当前页(标题/图可选)
分享卡片同步协议(location/share/setTitle/copyLink/requestProfile)见 分享卡片同步协议(location/share/setTitle/copyLink/requestProfile)见
@@ -26,6 +28,68 @@
return m ? decodeURIComponent(m[1]) : ''; return m ? decodeURIComponent(m[1]) : '';
} catch (e) { return ''; } } catch (e) { return ''; }
} }
/* ============================================================
同源身份缓存 —— openid / unionid 的兜底通道(2026-09-22)
为什么需要:H5 应用里「返回首页」这类跳转是**页面自己发起的同源跳转**,
宿主的 <web-view src> 属性不变,所以宿主那条 _applyIdentity() 注入**不会触发**,
目标页(mama-box)拿到的 URL 里没有 openid → 只能显示「身份不完整」。
⚠️ 而且这一跳**永远不可能带上身份**:孕检日历那类应用会把 openid/phone
从自己的 URL 上主动擦掉(防分享链接冒名顶替,理由见它的注释)——
所以对第三方应用来说,这条缓存是**不改它们代码的唯一出路**。
做法:mp.yizhezhe.com 下所有应用与 mama-box 是**同源**(实测在架 12 个应用
全在该域名),共用一份 sessionStorage。宿主注入过的身份顺手存一份,
读取时 **URL 优先 → 缓存兜底**。
三条纪律(改这个文件前先读):
① **URL 永远权威**:只要 URL 里带 openid 就无条件覆盖缓存 ——
绝不做「有缓存就不读 URL」,否则切号/换人后会串号;
② **只在本 origin 内有效**:App 表允许填外链,外链应用读不到这份缓存,
所以「跳转要带身份」的规范不能删,缓存只是兜底;
③ **只存 openid/unionid**(服务端身份键),不碰 token / 手机号 / 昵称头像。
存 sessionStorage 而不是 localStorage:报告的场景都在同一次会话内,
sessionStorage 够用,且随标签页结束就没了,暴露窗口小得多。
============================================================ */
var ID_KEY = 'mmbox.identity';
var ID_TTL = 12 * 3600 * 1000; // 12h:sessionStorage 本就随会话结束,这层防的是长驻旧值
function rememberIdentity(oid, uid) {
if (!oid) return;
try {
sessionStorage.setItem(ID_KEY, JSON.stringify({ openid: oid, unionid: uid || '', at: Date.now() }));
} catch (e) { /* 隐私模式等:静默,不影响主流程 */ }
}
function cachedIdentity() {
try {
var c = JSON.parse(sessionStorage.getItem(ID_KEY) || 'null');
if (!c || !c.openid) return null;
if (c.at && (Date.now() - c.at) > ID_TTL) return null;
return c;
} catch (e) { return null; }
}
/* 读身份:URL 优先 → 缓存兜底。命中缓存时打一条日志(口径见接入规范第 5 节:
缓存会掩盖「宿主没注入身份」这个本来会显性提示的信号,所以留痕便于排查)。 */
function identity() {
/* ⚠️ 与 mama-box 的 hashParam 同款护栏:历史 bug 会把缓存戳粘进 openid
(abc001?t=123),真 openid 不含 ?,剥掉以兼容被污染的旧链接。 */
var oid = String(readHash('openid') || '').split('?')[0];
var uid = String(readHash('unionid') || '').split('?')[0];
if (oid) { rememberIdentity(oid, uid); return { openid: oid, unionid: uid, from: 'url' }; }
var c = cachedIdentity();
if (c) {
try {
console.log('[wxAuth] URL 未带身份 → 用同源缓存兜底:openid=' + c.openid +
(c.unionid ? ' unionid=' + c.unionid : '') + '(写入于 ' + new Date(c.at).toLocaleTimeString() + ')');
} catch (e) {}
return { openid: c.openid, unionid: c.unionid || '', from: 'cache' };
}
return { openid: '', unionid: '', from: 'none' };
}
/* 载入即采一次:**接入方不需要改任何代码** —— 只要页面引了本库,
宿主注入过的身份就会被存下来(孕检日历那类第三方应用正是靠这一条自己存下身份的)。 */
(function captureAtLoad() { try { identity(); } catch (e) {} })();
var wxAuth = { var wxAuth = {
MODE: 'browser', // 'webview' | 'wechat-browser' | 'browser' MODE: 'browser', // 'webview' | 'wechat-browser' | 'browser'
inWebview: false, inWebview: false,
@@ -146,13 +210,34 @@
this.toast('请在小程序中打开商城'); this.toast('请在小程序中打开商城');
} }
}, },
/* ⑥ 展示授权结果到指定元素(可选便捷方法) */ /* ⑥ 读当前身份(openid/unionid):URL 优先 → 同源缓存兜底。
返回 {openid, unionid, from:'url'|'cache'|'none'}。
用途:本页自己也需要 openid 时(请求后端、拼跳转链接)统一走它,
顺带把身份写进缓存 —— 别在各应用里各写一份读参数的代码。 */
identity: identity,
/* 手动把身份写进缓存(一般不用调:URL 带身份时 identity() 已自动写)。
场景:授权回传后身份只在 hash 里出现一次,若随后要清 URL(防分享泄露),
可先调它存一份。 */
rememberIdentity: rememberIdentity,
/* ⑦ 回 mama-box 首页(H5 方式)。带身份:URL 优先,其次缓存。
⚠️ 「返回首页」这类跳转**不要**裸写 location.href='/mama-box/' ——
宿主不会因此重新注入身份(web-view src 没变),目标页会显示「身份不完整」,
而重试也救不回来。要么调本方法,要么保证链接带上 openid/unionid。
(第三方应用改不了的话,靠 identity() 的缓存兜底也能让首页认出人来。) */
goHome: function () {
var id = identity();
var q = id.openid
? '?openid=' + encodeURIComponent(id.openid) + (id.unionid ? '&unionid=' + encodeURIComponent(id.unionid) : '')
: '';
location.href = 'https://mp.yizhezhe.com/mama-box/' + q;
},
/* ⑧ 展示授权结果到指定元素(可选便捷方法) */
fillAuth: function (nameEl, avatarEl) { fillAuth: function (nameEl, avatarEl) {
var a = this.readAuth(); var a = this.readAuth();
if (nameEl && a.nickname) nameEl.textContent = a.nickname; if (nameEl && a.nickname) nameEl.textContent = a.nickname;
if (avatarEl && a.avatar) avatarEl.src = a.avatar; if (avatarEl && a.avatar) avatarEl.src = a.avatar;
}, },
/* ⑦ 上报"当前在哪一页"(分享状态同步,不弹 toast、可频繁调用)。 /* ⑨ 上报"当前在哪一页"(分享状态同步,不弹 toast、可频繁调用)。
为什么需要:web-view 里的 H5 不能自己拉起分享面板,转发卡片由小程序宿主页产出; 为什么需要:web-view 里的 H5 不能自己拉起分享面板,转发卡片由小程序宿主页产出;
宿主只有 onShareAppMessage(options).webViewUrl 能拿到当前地址,而 宿主只有 onShareAppMessage(options).webViewUrl 能拿到当前地址,而
onShareTimeline(朋友圈)**没有回调参数**;首页壳又不会随 H5 换页重新 onLoad。 onShareTimeline(朋友圈)**没有回调参数**;首页壳又不会随 H5 换页重新 onLoad。