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

257 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 微信 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 参数)
**代码片段:**
```javascript
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)`**
```javascript
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
```javascript
console.log('pendingRoute:', authStore.pendingRoute);
```
### **Q2为什么一直在授权页面循环**
**A** 可能是授权回调处理失败。检查:
1. 后端 `/user/wxh5Auth` 接口是否正确返回 token
2. Token 是否正确保存到 localStorage
3. URL 中的 code/state 是否正确清理
### **Q3为什么刷新页面后授权状态丢失**
**A** authStore.restoreFromStorage() 应该在 onMounted 中被调用。检查:
```javascript
authStore.restoreFromStorage(); // 这行很重要!
```
### **Q4在 PC 上测试,为什么无法触发授权?**
**A** 授权只在微信环境中触发isWeChat.value === true。在 PC 上:
- 访问需登录的页面 → 跳转登录页
- 访问开放页面 → 正常加载
---
## 📝 总结
这个重构的核心思想是:
1. **路由守卫 = 决策者**:检测到需要授权就立即发起
2. **App.vue = 处理器**:处理授权回调和状态恢复
3. **AuthStore = 状态管理**:保存和恢复授权相关的状态
4. **同步流程 = 高可靠性**:避免异步竞态条件
通过这个设计,微信授权流程变得:
- ✅ 清晰易懂
- ✅ 可靠稳定
- ✅ 易于测试和调试
- ✅ 完整的用户体验