Files
ycc-proxy-webview/微信授权流程设计文档.md
2025-12-16 19:27:20 +08:00

11 KiB
Raw Blame History

微信 H5 授权流程重构 - 设计文档

📋 核心业务需求

在微信 H5 环境中,整个应用在没有有效 token 时,都需要进行微信授权登录。授权完成后,用户应该被重定向回他们尝试访问的原始页面。


🔄 流程设计

完整的授权流程图

┌─────────────────────────────────────────────────────────────┐
│ 用户在微信中访问任意页面(无 token                         │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ 路由守卫检测:微信 + 无 token                                │
│ → 保存目标路由到 authStore.pendingRoute                    │
│ → 生成微信授权 URL                                          │
│ → window.location.href = 授权 URL (不调用 next()         │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ 跳转到微信授权页面                                           │
│ https://open.weixin.qq.com/connect/oauth2/authorize?...   │
└──────────────────┬──────────────────────────────────────────┘
                   │
         ┌─────────┴─────────┐
         │ 用户点击"同意"    │
         └────────┬──────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ 微信回调redirectUri?code=xxx&state=yyy                    │
│ 浏览器重新加载应用URL 中包含 code/state 参数              │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ App.vue onMounted 检测到 code + state 参数                  │
│ → 调用 handleWeixinAuthCallback(code)                       │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ 1. 调用后端接口 /user/wxh5Auth 交换 token                   │
│ 2. 保存 token 到 localStorage                               │
│ 3. 清理 URL 中的 code/state 参数                            │
│ 4. 获取用户信息                                             │
│ 5. 标记授权完成 authStore.completeWeixinAuth()             │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ 获取 pendingRoute重定向回原始页面                         │
│ router.replace(pendingRoute)                                │
│ 如果没有 pendingRoute重定向到首页 "/"                     │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│ ✅ 授权完成,用户看到原始页面                                │
└─────────────────────────────────────────────────────────────┘

📁 关键文件修改

1. src/router/index.js - 路由守卫

关键变化:

  • 检测到 微信 + 无 token 的情况
  • 直接发起授权(调用 window.location.href
  • 不调用 next(),完全停止导航
  • 授权 URL 中的 redirectUri 指向当前页面(清理旧的 code/state 参数)

代码片段:

if (isWeChat.value && !isAuthenticated && !isTokenExpired) {
    // 保存目标路由
    authStore.startWeixinAuth(to);

    // 生成授权 URL
    const appId = import.meta.env.VITE_WECHAT_APP_ID;
    const url = new URL(window.location.href);
    const params = new URLSearchParams(url.search);
    params.delete("code");
    params.delete("state");
    const cleanUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`;
    const redirectUri = encodeURIComponent(cleanUrl);
    const weixinAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=snsapi_base#wechat_redirect`;

    // 直接跳转,不调用 next()
    window.location.href = weixinAuthUrl;
    return;
}

2. src/App.vue - 授权回调处理

关键职责:

  1. 检测回调:读取 URL 中的 code/state 参数
  2. 处理回调:调用 /user/wxh5Auth 交换 token
  3. 恢复导航:使用保存的 pendingRoute 重定向用户

核心函数:handleWeixinAuthCallback(code)

const handleWeixinAuthCallback = async (code) => {
    // 1. 交换 token
    const { data, error } = await useApiFetch("/user/wxh5Auth").post({ code }).json();
    
    // 2. 保存 token
    localStorage.setItem("token", data.value.data.accessToken);
    localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
    localStorage.setItem("accessExpire", data.value.data.accessExpire);
    
    // 3. 清理 URL
    const url = new URL(window.location.href);
    const params = new URLSearchParams(url.search);
    params.delete("code");
    params.delete("state");
    window.history.replaceState({}, "", newUrl);
    
    // 4. 获取用户信息
    await userStore.fetchUserInfo();
    
    // 5. 标记授权完成
    authStore.completeWeixinAuth();
    
    // 6. 重定向回 pendingRoute
    const pendingRoute = authStore.pendingRoute;
    await router.replace(pendingRoute || "/");
};

3. src/stores/authStore.js - 授权状态管理

关键方法:

方法 作用
startWeixinAuth(targetRoute) 开始授权,保存目标路由
completeWeixinAuth() 标记授权完成
clearPendingRoute() 清除待处理路由
resetAuthState() 重置所有授权状态
restoreFromStorage() 页面刷新后恢复状态

🎯 核心特性

1. 同步流程控制

  • 不使用 watch 监听 state 的变化
  • 不使用 异步的 route guard + next() 来触发授权
  • 直接 在 route guard 中调用 window.location.href 发起授权

为什么?

  • Watch 是异步的,可能被路由完成打断
  • next() 后可能路由已经切换,导致授权流程混乱
  • 直接跳转 URL 更加可靠和同步

2. 状态持久化

  • 授权开始时保存到 localStorage
  • 页面刷新时自动恢复状态
  • 防止超时30秒导致的无限授权循环

3. URL 参数清理

  • 每次发起授权前,都清理 URL 中旧的 code/state 参数
  • 防止参数被重复编码进 redirectUri
  • 微信回调时才注入新的 code/state

4. 目标页面恢复

  • 路由守卫保存用户尝试访问的页面
  • 授权完成后自动重定向到该页面
  • 用户无感知的完整授权体验

🔐 场景处理

场景 1无 token + 访问开放页面

用户访问 "/" → 微信检测无 token → 发起授权 → 授权完成后回到首页

场景 2无 token + 访问推广链接

用户访问 "/agent/promotionInquire/abc123"
  → 微信检测无 token
  → 保存 pendingRoute = "/agent/promotionInquire/abc123"
  → 发起授权
  → 授权完成后回到 "/agent/promotionInquire/abc123"

场景 3有效 token + 访问任意页面

用户有 token 且未过期 → 路由守卫放行 → 直接加载页面,无授权

场景 4Token 过期 + 访问需登录页面

用户在非微信环境 → 检测到 token 过期 → 跳转登录页面

场景 5授权超时

用户授权流程中30 秒内未完成 → 自动重置状态 → 页面刷新时重新授权

🐛 常见问题排查

Q1为什么授权后没有跳到原页面

A 检查 authStore 中是否有 pendingRoute

console.log('pendingRoute:', authStore.pendingRoute);

Q2为什么一直在授权页面循环

A 可能是授权回调处理失败。检查:

  1. 后端 /user/wxh5Auth 接口是否正确返回 token
  2. Token 是否正确保存到 localStorage
  3. URL 中的 code/state 是否正确清理

Q3为什么刷新页面后授权状态丢失

A authStore.restoreFromStorage() 应该在 onMounted 中被调用。检查:

authStore.restoreFromStorage(); // 这行很重要!

Q4在 PC 上测试,为什么无法触发授权?

A 授权只在微信环境中触发isWeChat.value === true。在 PC 上:

  • 访问需登录的页面 → 跳转登录页
  • 访问开放页面 → 正常加载

📝 总结

这个重构的核心思想是:

  1. 路由守卫 = 决策者:检测到需要授权就立即发起
  2. App.vue = 处理器:处理授权回调和状态恢复
  3. AuthStore = 状态管理:保存和恢复授权相关的状态
  4. 同步流程 = 高可靠性:避免异步竞态条件

通过这个设计,微信授权流程变得:

  • 清晰易懂
  • 可靠稳定
  • 易于测试和调试
  • 完整的用户体验