fix(wx): strip identity from share page_path; guard router unionid switch

- Sanitize page_path before router.push (H5) to prevent cross-user login via shared link
- onShareAppMessage: remove unionid/openid/status and related query keys from shared path
- main.ts: fix login(to.query.toString()) bug; when already logged in, strip foreign unionid from URL instead of logout

Made-with: Cursor
This commit is contained in:
2026-04-26 15:58:56 +08:00
parent 3a6b9b6973
commit b1996f3cd1
4 changed files with 90 additions and 7 deletions
+22 -4
View File
@@ -72,12 +72,30 @@ router.beforeEach(async (to, from , next) => {
} }
} }
} }
// switched unionid // switched unionidURL 与当前会话不一致时)
if (to.query.unionid && store.unionid !== to.query.unionid) { if (to.query.unionid && store.unionid !== to.query.unionid) {
cookie.set(HtyUnionIDToken, to.query.unionid); const rawQ = to.query.unionid;
const qUnionStr = String(Array.isArray(rawQ) ? rawQ[0] : rawQ || '');
// 已登录且 URL 中的 unionid 与当前用户不同:多为带毒分享链接,剥离身份参数而非 logout
if (store.current.hty_id && qUnionStr) {
const cur = store.current as { union_id?: string };
const currentUnion = String(
cur.union_id != null && cur.union_id !== '' ? cur.union_id : store.unionid || ''
);
if (currentUnion && qUnionStr !== currentUnion) {
const nextQuery = { ...to.query } as Record<string, string | string[] | undefined | null>;
delete nextQuery.unionid;
delete nextQuery.openid;
delete nextQuery.status;
await router.replace({ path: to.path, query: nextQuery });
next();
return;
}
}
cookie.set(HtyUnionIDToken, qUnionStr);
logout(); logout();
await login(to.query.toString()) await login(qUnionStr);
next() next();
return; return;
} }
let {enabled, is_registered, roles, union_id} = store.current; let {enabled, is_registered, roles, union_id} = store.current;
+3 -1
View File
@@ -50,6 +50,7 @@ import {
showConfirmDialog, showConfirmDialog,
} from "vant"; } from "vant";
import { CurrentUserRole } from "~/utils"; import { CurrentUserRole } from "~/utils";
import { sanitizeMiniProgramWebViewPathForRouter } from "~/utils/wxMiniProgramWebViewShare";
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -97,7 +98,8 @@ export default defineComponent({
console.log("page_path...", page_path, decodeURIComponent(page_path)) console.log("page_path...", page_path, decodeURIComponent(page_path))
if (page_path) { if (page_path) {
await router.push(decodeURIComponent(page_path)) const inner = sanitizeMiniProgramWebViewPathForRouter(decodeURIComponent(page_path))
await router.push(inner)
return; return;
} }
+42
View File
@@ -0,0 +1,42 @@
/**
* 小程序 WebView 分享 / page_path 里可能携带上一用户的 unionid、openid、status 等,
* 接收方打开后会导致串号或误登。从「路径 + 查询」字符串中剥离这些参数后再交给 vue-router。
*/
const WEBVIEW_IDENTITY_QUERY_KEYS = [
'unionid',
'openid',
'status',
'nickName',
'avatarUrl',
'ts',
'scene',
] as const;
export function sanitizeMiniProgramWebViewPathForRouter(decodedPathOrUrl: string): string {
const raw = decodedPathOrUrl.trim();
if (!raw || raw === '/') {
return '/';
}
let pathPart = raw;
let search = '';
const q = raw.indexOf('?');
if (q === 0) {
pathPart = '/';
search = raw.slice(1);
} else if (q > 0) {
pathPart = raw.slice(0, q) || '/';
search = raw.slice(q + 1);
}
if (!pathPart.startsWith('/')) {
pathPart = `/${pathPart}`;
}
if (!search) {
return pathPart;
}
const params = new URLSearchParams(search);
for (const key of WEBVIEW_IDENTITY_QUERY_KEYS) {
params.delete(key);
}
const rest = params.toString();
return rest ? `${pathPart}?${rest}` : pathPart;
}
+23 -2
View File
@@ -211,8 +211,29 @@ Page({
onShareAppMessage({from, target, webViewUrl}) { onShareAppMessage({from, target, webViewUrl}) {
log.info("share", [from, target, webViewUrl].join(" | ")) log.info("share", [from, target, webViewUrl].join(" | "))
let pathname = webViewUrl.replace(app.globalData.server, ''); let pathname = webViewUrl.replace(app.globalData.server, '');
if (pathname !== '/') { // 分享勿带 WebView 入口里的 unionid/openid/status 等,避免接收方串号
pathname = encodeURIComponent(pathname) if (pathname && pathname !== '/') {
try {
const q = pathname.indexOf('?')
let base = pathname
let search = ''
if (q === 0) {
base = '/'
search = pathname.slice(1)
} else if (q > 0) {
base = pathname.slice(0, q) || '/'
search = pathname.slice(q + 1)
}
if (search) {
const sp = new URLSearchParams(search)
;['unionid', 'openid', 'status', 'nickName', 'avatarUrl', 'ts', 'scene'].forEach((k) => sp.delete(k))
const rest = sp.toString()
pathname = rest ? `${base}?${rest}` : (base || '/')
}
} catch (e) {
log.info('share strip query skip', e)
}
pathname = pathname === '/' ? '/' : encodeURIComponent(pathname)
} }
const promise = new Promise(resolve => { const promise = new Promise(resolve => {
setTimeout(() => { setTimeout(() => {