## 方案:小程序跳转到 H5 页面携带 token ### Summary - 目标:在微信小程序内通过 web-view 打开指定 H5 地址时,让 H5 自动获得与小程序一致的登录态(token),并在 H5 内完成 Vuex/缓存写入与用户信息同步。 - 约束(来自现状与确认):打开方式为 web-view;当前不方便改后端接口;倾向直接携带 Bearer token。 - 推荐策略:优先把 token 放到 URL 的 fragment(`#` 之后)以避免出现在 HTTP 请求与 Referer/服务端日志里;H5 启动时从 fragment 解析 token,写入 `LOGIN_STATUS` 缓存并拉取用户信息,随后清理 URL 中的 token。 ### Current State Analysis - 小程序打开 H5 的通用入口:`utils/util.js: JumpPath`,当 url 包含 `http` 时在 MP/APP-PLUS 走 web-view 页面: - [util.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/utils/util.js#L162-L201) - web-view 页面接收 `option.url` 并 `decodeURIComponent` 后赋给 ``: - [pages/annex/web_view/index.vue](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/pages/annex/web_view/index.vue#L1-L28) - H5 端已有“从 URL query 自动登录”的机制,但目前只处理 `remote_token`: - H5 `App.vue`:`if (!isLogin && option.query.hasOwnProperty('remote_token')) this.remoteRegister(...)` - [App.vue](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/App.vue#L88-L92) - `remoteRegister` 调用 `GET /api/remote_register`(noAuth)换取 token 或跳转 URL: - [api/public.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/api/public.js#L231-L239) - token 使用方式:请求层统一在 `utils/request.js` 注入 header `Authori-zation: Bearer `: - [request.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/utils/request.js#L53-L63) - 登录态落地:`store/modules/app.js: LOGIN` 将 token 写入 `Cache.set(LOGIN_STATUS, token, time)`;并可配合 `getUserInfo()` 写入 uid/userInfo: - [app.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/store/modules/app.js#L89-L117) ### Proposed Changes #### 1) 小程序侧:构造带 token 的 H5 URL(尽量放在 fragment) - 修改文件:`utils/util.js` - 修改点:在 `JumpPath` 的 `url.indexOf("http") != -1` 分支(MP/APP-PLUS)中,在跳转到 `/pages/annex/web_view/index` 之前对目标 url 做“携带 token”的包装。 - 行为设计: - 若用户未登录(`store.getters.isLogin` 为 false 或 `store.state.app.token` 为空),直接走现有登录流程(可复用 `libs/login.js: toLogin`),不打开 H5。 - 若已登录: - 优先把 token 附加在 hash 内:`https://h5.xxx.com/#/some/path?token=` - 若目标 url 不含 `#`,则退化为 query:`https://h5.xxx.com/some/path?token=`(存在服务端日志/Referer 风险,属于“不得不”的兼容路径)。 - 在把 url 作为参数传给 web_view 页面时,对整段 url 做 `encodeURIComponent`,避免 token 中的特殊字符以及 `&` 被小程序路由二次解析。 - 预计新增/调整函数(放在 util.js 内部): - `appendTokenToUrl(rawUrl, token): string`(只做字符串处理,不引入新依赖) - `encodeForWebView(url): string`(统一 `encodeURIComponent`) - 影响面: - 任何通过 `this.$util.JumpPath('https://...')` 打开的 H5 链接,都将自动带上 token(仅在 MP/APP-PLUS 生效)。 #### 2) web-view 容器页:强制 decode 前提下保证传参完整 - 修改文件:`pages/annex/web_view/index.vue` - 修改点:保持现有 `decodeURIComponent(option.url)`,但要求所有调用方都传入 encode 后的 url。 - 如果项目内存在“未 encode 直接拼接 url”的调用点(当前 `utils/util.js` 即如此),统一改为 encode 后再传。 #### 3) H5 侧:解析 token → 写入登录态 → 拉取用户信息 → 清理 URL - 修改文件:`App.vue` - 修改点:H5 `onLaunch` 内(`// #ifdef H5` 分支)补充 token 自动登录逻辑,优先级建议: 1. 若当前已登录(`this.$store.getters.isLogin` 为 true),不处理外部 token(避免覆盖用户主动登录态)。 2. 仍保留现有 `remote_token` 分支逻辑(兼容已有能力)。 3. 新增 `token` 分支逻辑: - 从 `option.query.token` 读取(query 方案) - 若取不到,再从 `location.hash` 里解析 `token`(fragment 方案) - 拿到 token 后: - `this.$store.commit("LOGIN", { token, time: 0 })`(time=0 表示缓存不主动过期,后续由服务端 410xxx 兜底踢下线) - 调用 `getUserInfo()`,成功后 `SETUID/UPDATE_USERINFO` - 最后用 `history.replaceState` 把 URL 中的 token 移除(避免用户复制链接时携带 token) - 需要补一个轻量解析方法(不引入库): - `parseTokenFromHash(hash): string | ''`:兼容 `#/path?token=...` 和 `#/?token=...` 两种。 ### Assumptions & Decisions - H5 地址可能是 uni-app 默认 hash 路由(`/#/`);因此默认把 token 放在 fragment 内最安全。 - 不改后端时,“直接 token”不可避免有泄露风险;通过 fragment + 清理 URL 将风险降到相对可接受: - fragment 不会出现在 HTTP 请求行与一般服务端 access log - Referer 通常不包含 fragment - token 仍可能在以下场景泄露:用户截屏、设备调试、前端错误上报采集 `location.href` 等;因此上线前需确认前端埋点/监控不会上报完整 URL。 ### Verification - 本地/测试环境: - 小程序端登录后,通过任意入口打开一个 H5 链接(走 `JumpPath`): - web-view 打开的 H5 页面无需再次登录 - H5 内 `store.getters.isLogin` 为 true,且请求头带 `Authori-zation: Bearer ...` - `getUserInfo()` 可正常返回并写入 uid/userInfo - 打开后检查 H5 当前地址:token 参数已被移除(history.replaceState 生效) - 回归: - 未登录时打开 H5 链接:应先跳登录,不应打开带 token 的 H5 - H5 原有 `remote_token` 登录不受影响