mama-box: 共享授权规范闭环 — box_app UserAuth + /api/mp/public/auth/profile;wx-h5-demo 用 wx-auth.js 演示标准流程

This commit is contained in:
2026-09-07 19:57:57 +08:00
parent bc3531e83c
commit e8e577b621
2 changed files with 121 additions and 69 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#F4593C">
<title>wx-h5-demo · 小程序授权演示(独立项目)</title>
<title>wx-h5-demo · 共享授权规范示例</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
body { font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#F8F3EF; color:#2B2320; min-height:100vh; display:flex; justify-content:center; }
@@ -12,7 +12,7 @@
.brand { font-size:22px; font-weight:800; color:#F4593C; } .brand small{color:#A79C95;font-size:13px;font-weight:500;margin-left:8px}
.env { margin:14px 0 8px; font-size:13px; color:#6B615B; } .env b{color:#2B2320}
.tag { display:inline-block; padding:2px 8px; border-radius:8px; font-size:12px; }
.tag.s0{background:#E8F2EB;color:#5F8A4A} .tag.s1{background:#FDEEDD;color:#B8860B} .tag.s2{background:#F6DCD6;color:#C4553A}
.tag.ok{background:#E8F2EB;color:#5F8A4A} .tag.no{background:#F6DCD6;color:#C4553A}
.card { background:#fff; border:1px solid #EFE7E1; border-radius:16px; padding:16px; margin-top:14px; }
.card h2{font-size:15px;font-weight:800;margin-bottom:10px}
.row{display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px dashed #EFE7E1;font-size:14px}
@@ -28,97 +28,66 @@
</head>
<body>
<div class="wrap">
<div class="brand">wx-h5-demo <small>独立示例 · 小程序授权</small></div>
<div class="env">场景:<b id="envTxt">检测中…</b> <span id="envTag" class="tag s2">纯浏览器</span></div>
<div class="brand">wx-h5-demo <small>共享授权规范示例</small></div>
<div class="env">环境:<b id="envTxt">检测中…</b> <span id="envTag" class="tag no">纯浏览器</span></div>
<div class="card">
<h2>当前可用的授权方式</h2>
<p id="scenarioTxt" style="font-size:13px;color:#6B615B;line-height:1.6">检测环境后,这里会给出「场景1 或 场景2」对应的授权入口。</p>
<button class="btn main" id="btnJump">去小程序授权</button>
<button class="btn ghost" id="btnPost">在 web-view 里请求授权(postMessage)</button>
<h2>授权状态</h2>
<p id="authState" style="font-size:13px;color:#6B615B;line-height:1.6">检测中…</p>
<button class="btn main" id="btnAuth" style="display:none">授权登录</button>
<button class="btn ghost" id="btnMall">进商城</button>
</div>
<div class="card">
<h2>授权结果(小程序回传后读取)</h2>
<h2>授权结果(宿主页回传后读取 #phone/nickname/avatar)</h2>
<div class="row"><span class="k">手机号</span><span class="v placeholder" id="rPhone">—</span></div>
<div class="row"><span class="k">昵称</span><span class="v placeholder" id="rNick">—</span></div>
<div class="row"><span class="k">头像</span><span class="v"><span class="avatar" id="rAvatarWrap"><span id="rAvatarText">宝</span></span></span></div>
</div>
<p class="hint">
场景1(微信浏览器打开 H5):点「去小程序授权」→ 经 <code>wx-open-launch-weapp</code> / 小程序 <code>URL Scheme</code> 跳进小程序授权页 → 授权后,小程序用 <code>web-view</code> 加载本 H5(结果在 URL),即「回到 H5」。
场景2(web-view 里的 H5):点「在 web-view 里请求授权」→ <code>postMessage</code> → 宿主小程序弹原生授权面板 → 更新 <code>web-view src</code>(带 #phone/nickname/avatar)→ 本页重载显示。
完整说明见 <code>docs/h5-miniprogram-auth.md</code>;小程序端配套代码见本目录 <code>miniapp/</code>。
本页演示<b>共享授权规范</b>:<code>wx-auth.js</code>(<code>wxAuth.init/requestProfile/goMall/readAuth/isAuthorized</code>)。
授权流程:点「授权登录」→ <code>wx.miniProgram.postMessage({action:'requestProfile'})</code> →
通用宿主页 <code>pages/annex/web_view</code> 弹原生授权面板(头像/昵称/手机号)→ <code>POST /api/mp/public/auth/profile</code> 落库共享 UserAuth →
宿主页更新 <code>web-view src</code>(hash 带结果)→ 本页重载 → <code>wxAuth.readAuth()</code> 显示。
「进商城」→ <code>wx.miniProgram.switchTab('/pages/goods_cate/goods_cate')</code>。
</p>
</div>
<div id="toast"></div>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script src="wx-auth.js"></script>
<script>
(function () {
'use strict';
function $(s) { return document.querySelector(s); }
function toast(msg) { var t=$('#toast'); t.textContent=msg; t.classList.add('show'); clearTimeout(t._t); t._t=setTimeout(function(){t.classList.remove('show');},1800); }
function readParam(name){ try{ var h=location.hash.slice(1)+'&'+location.search.slice(1); var m=h.match(new RegExp('(?:^|[&#])'+name+'=([^&#]*)(?:$|[&#])')); return m?decodeURIComponent(m[1]):''; }catch(e){return '';} }
var MODE = ''; // 'webview' | 'wechat-browser' | 'browser'
var isWechat = /MicroMessenger/i.test(navigator.userAgent);
var inWebview = false;
function setEnv(mode, label, cls) {
MODE = mode; inWebview = (mode === 'webview');
$('#envTxt').textContent = label; $('#envTag').textContent = label; $('#envTag').className = 'tag ' + cls;
var txt = '';
if (mode === 'webview') txt = '场景2:本页运行在小程序 web-view 里。点下方「在 web-view 里请求授权」→ postMessage 通知宿主小程序弹原生授权面板;授权后宿主更新 web-view src 回传,本页重载显示结果。';
else if (mode === 'wechat-browser') txt = '场景1:本页在微信浏览器打开(非 web-view)。点「去小程序授权」→ 跳进小程序授权页授权;授权后小程序用 web-view 加载本 H5(结果在 URL)即回到 H5。';
else txt = '纯浏览器:仅演示 UI,无法真授权(去小程序按钮/场景2需在微信内)。';
$('#scenarioTxt').textContent = txt;
function show(tag, title) {
$('#envTag').textContent = title; $('#envTag').className = 'tag ' + (tag === 'ok' ? 'ok' : 'no');
}
function detectEnv() {
if (window.wx && window.wx.miniProgram) {
var responded = false;
window.wx.miniProgram.getEnv(function (r) { responded = true; setEnv(r && r.miniprogram ? 'webview' : (isWechat ? 'wechat-browser' : 'browser'), r && r.miniprogram ? '小程序 web-view' : (isWechat ? '微信浏览器' : '纯浏览器'), r && r.miniprogram ? 's0' : (isWechat ? 's1' : 's2')); });
setTimeout(function () { if (!responded) setEnv(isWechat ? 'wechat-browser' : 'browser', isWechat ? '微信浏览器' : '纯浏览器', isWechat ? 's1' : 's2'); }, 800);
} else {
setEnv(isWechat ? 'wechat-browser' : 'browser', isWechat ? '微信浏览器' : '纯浏览器', isWechat ? 's1' : 's2');
}
function render(a) {
// 已授权 → 展示;未授权 → 显示「授权登录」
var authed = a.isAuthorized();
$('#authState').textContent = authed ? '✅ 已授权(昵称/头像/手机号)' : '🔓 未授权,点下方「授权登录」';
$('#btnAuth').style.display = authed ? 'none' : 'block';
// 展示回传结果
$('#rPhone').textContent = a.readAuth().phone || '—';
$('#rNick').textContent = a.readAuth().nickname || '—';
if (a.readAuth().avatar) { $('#rAvatarWrap').innerHTML = '<img src="' + a.readAuth().avatar + '" alt="头像">'; }
}
// 场景1:跳小程序授权(wx-open-launch-weapp 或 URL Scheme) → 小程序授权页
var LAUNCH_PATH = '/pages/demo/auth'; // 小程序授权页路径(场景1 的授权页,见 miniapp/scenario1-auth-bridge.*)
var LAUNCH_SCHEME = ''; // 微信浏览器用的 URL Scheme(后端用 appid+path 生成,见 README);留空则走 webview navigateTo
function jumpToMiniAuth() {
if (MODE === 'browser') { toast('请在微信内打开'); return; }
// 小程序 web-view 内:用 wx.miniProgram.navigateTo 跳到授权页(场景1 在 webview 里的做法)
if (window.wx && window.wx.miniProgram && inWebview) {
window.wx.miniProgram.navigateTo({ url: LAUNCH_PATH });
return;
}
// 微信浏览器(非 web-view):优先 URL Scheme;否则提示用 <wx-open-launch-weapp>(需 jsapi 签名)
if (LAUNCH_SCHEME) { location.href = LAUNCH_SCHEME; return; }
toast('请配置小程序 URL Scheme(后端生成)或用 <wx-open-launch-weapp> 跳授权页');
function init() {
wxAuth.init(function (a) {
// 环境
var envTxt = a.inWebview ? '小程序 web-view' : (a.MODE === 'wechat-browser' ? '微信浏览器' : '纯浏览器');
$('#envTxt').textContent = envTxt;
// 环境 tag:已授权才 ok
render(a);
});
// 授权后(宿主更新 src,本页重载)→ 由 init 里 readAuth 拿到
}
// 场景2:postMessage 请求 web-view 宿主弹授权面板
function requestAuthInWebview() {
if (MODE !== 'webview') { toast('仅 web-view 环境可用(场景2)'); return; }
window.wx.miniProgram.postMessage({ data: { action: 'requestProfile' } });
toast('请在弹出的小程序面板授权');
}
function renderResult() {
var phone = readParam('phone'), nick = readParam('nickname'), avatar = readParam('avatar'), authed = readParam('authed');
if (phone) { $('#rPhone').textContent = phone; $('#rPhone').classList.remove('placeholder'); }
if (nick) { $('#rNick').textContent = nick; $('#rNick').classList.remove('placeholder'); }
if (avatar){ $('#rAvatarWrap').innerHTML = '<img src="' + avatar + '" alt="头像">'; }
if (authed) toast('授权完成,已回传');
}
document.addEventListener('DOMContentLoaded', function () {
detectEnv(); renderResult();
$('#btnJump').addEventListener('click', jumpToMiniAuth);
$('#btnPost').addEventListener('click', requestAuthInWebview);
init();
$('#btnAuth').addEventListener('click', function () { wxAuth.requestProfile(); });
$('#btnMall').addEventListener('click', function () { wxAuth.goMall(); });
});
})();
</script>

83
wx-auth.js Normal file
View File

@@ -0,0 +1,83 @@
/* ============================================================
wx-auth.js · H5 应用通用授权 + 跳商城 共享助手(各 H5 应用引用即可)
------------------------------------------------------------
依赖:页面引入 <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
用法:在 H5 页面里
<script src="wx-auth.js"></script>
然后:
wxAuth.init(); // 检测环境 + 读取已授权资料(hash) + 回填
wxAuth.isAuthorized(); // 是否已授权(手机号/昵称/头像任一非空)
wxAuth.requestProfile(); // 点「授权登录」→ postMessage 通知宿主页
wxAuth.goMall(); // 点「进商城」→ 跳小程序商城 tabBar 页
回传协议:宿主页授权成功后更新 web-view src 为
<H5地址>?t=<ts>#openid=..&phone=..&nickname=..&avatar=..
(微信 webview 丢 query、不丢 hash,故授权结果放 hash)
============================================================ */
(function (g) {
'use strict';
function readHash(name) {
try {
var h = location.hash.slice(1) + '&' + location.search.slice(1);
var m = h.match(new RegExp('(?:^|[&#])' + name + '=([^&#]*)(?:$|[&#])'));
return m ? decodeURIComponent(m[1]) : '';
} catch (e) { return ''; }
}
var wxAuth = {
MODE: 'browser', // 'webview' | 'wechat-browser' | 'browser'
inWebview: false,
onReady: null,
/* ① 环境检测 + 读回传资料(授权后宿主更新 src,本页重载读到 hash) */
init: function (cb) {
this.onReady = cb || null;
var self = this;
var isWechat = /MicroMessenger/i.test(navigator.userAgent);
function setMode(m) {
self.MODE = m; self.inWebview = (m === 'webview');
if (self.onReady) self.onReady(self);
}
if (window.wx && window.wx.miniProgram) {
var responded = false;
window.wx.miniProgram.getEnv(function (r) {
responded = true;
setMode(r && r.miniprogram ? 'webview' : (isWechat ? 'wechat-browser' : 'browser'));
});
setTimeout(function () { if (!responded) setMode(isWechat ? 'wechat-browser' : 'browser'); }, 800);
} else {
setMode(isWechat ? 'wechat-browser' : 'browser');
}
},
/* ② 读取授权结果(hash 里的 phone/nickname/avatar) */
readAuth: function () {
return { phone: readHash('phone'), nickname: readHash('nickname'), avatar: readHash('avatar') };
},
/* ③ 是否已授权 */
isAuthorized: function () {
var a = this.readAuth();
return !!(a.nickname || a.avatar || a.phone);
},
/* ④ 请求授权:postMessage → 通用宿主页(annex/web_view)弹原生授权面板 */
requestProfile: function () {
if (!(window.wx && window.wx.miniProgram)) { this.toast('请在微信小程序中打开'); return; }
window.wx.miniProgram.postMessage({ data: { action: 'requestProfile' } });
this.toast('请在弹出的小程序面板授权');
},
/* ⑤ 跳小程序商城(tabBar 页用 switchTab;非 tabBar 用 navigateTo) */
goMall: function (url) {
if (!(window.wx && window.wx.miniProgram)) { this.toast('请在微信小程序中打开商城'); return; }
window.wx.miniProgram.switchTab({ url: url || '/pages/goods_cate/goods_cate' });
},
/* ⑥ 展示授权结果到指定元素(可选便捷方法) */
fillAuth: function (nameEl, avatarEl) {
var a = this.readAuth();
if (nameEl && a.nickname) nameEl.textContent = a.nickname;
if (avatarEl && a.avatar) avatarEl.src = a.avatar;
},
toast: function (m) {
var t = document.getElementById('toast');
if (!t) return;
t.textContent = m; t.classList.add('show');
clearTimeout(t._tt); t._tt = setTimeout(function () { t.classList.remove('show'); }, 1800);
}
};
g.wxAuth = wxAuth;
})(window);