257 lines
11 KiB
Markdown
257 lines
11 KiB
Markdown
|
|
# 微信 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 且未过期 → 路由守卫放行 → 直接加载页面,无授权
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### **场景 4:Token 过期 + 访问需登录页面**
|
|||
|
|
```
|
|||
|
|
用户在非微信环境 → 检测到 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. **同步流程 = 高可靠性**:避免异步竞态条件
|
|||
|
|
|
|||
|
|
通过这个设计,微信授权流程变得:
|
|||
|
|
- ✅ 清晰易懂
|
|||
|
|
- ✅ 可靠稳定
|
|||
|
|
- ✅ 易于测试和调试
|
|||
|
|
- ✅ 完整的用户体验
|