- 移动42个文档文件到 docs/ 目录 - 更新 .gitignore 允许 docs/ 下的 .md 文件 - 删除根目录下的重复文档文件 📁 文档分类: - StockDetailPanel 重构文档(3个) - PostHog 集成文档(6个) - 系统架构和API文档(33个) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1880 lines
48 KiB
Markdown
1880 lines
48 KiB
Markdown
# 价值前沿认证系统完整文档
|
||
|
||
> **版本**: 2.0
|
||
> **更新日期**: 2025-01-16
|
||
> **作者**: Claude Code
|
||
> **适用范围**: 前端 React + 后端 Flask
|
||
|
||
---
|
||
|
||
## 📖 目录
|
||
|
||
1. [系统架构概览](#1-系统架构概览)
|
||
2. [认证流程详解](#2-认证流程详解)
|
||
- [2.1 手机验证码登录](#21-手机验证码登录)
|
||
- [2.2 微信PC扫码登录](#22-微信pc扫码登录)
|
||
- [2.3 微信H5网页授权](#23-微信h5网页授权)
|
||
3. [路由配置与跳转逻辑](#3-路由配置与跳转逻辑)
|
||
4. [API接口文档](#4-api接口文档)
|
||
5. [前端组件架构](#5-前端组件架构)
|
||
6. [状态管理机制](#6-状态管理机制)
|
||
7. [Session持久化](#7-session持久化)
|
||
8. [错误处理策略](#8-错误处理策略)
|
||
9. [安全机制](#9-安全机制)
|
||
10. [调试指南](#10-调试指南)
|
||
|
||
---
|
||
|
||
## 1. 系统架构概览
|
||
|
||
### 1.1 技术栈
|
||
|
||
**前端**:
|
||
- React 18.3.1
|
||
- Chakra UI 2.8.2
|
||
- React Router 6.x
|
||
- Context API (状态管理)
|
||
|
||
**后端**:
|
||
- Flask (Python)
|
||
- Session-based Authentication
|
||
- HttpOnly Cookies
|
||
- Flask-Session
|
||
|
||
**认证方式**:
|
||
1. **手机验证码登录** (短信验证码)
|
||
2. **微信PC扫码登录** (二维码扫码)
|
||
3. **微信H5网页授权** (移动端跳转授权)
|
||
|
||
### 1.2 架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 用户界面层 (UI) │
|
||
│ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||
│ │ HomeNavbar │ │ AuthModal │ │ProtectedRoute│ │
|
||
│ │ (登录按钮) │ │ (认证弹窗) │ │ (路由保护) │ │
|
||
│ └───────────────┘ └──────────────┘ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 状态管理层 (Context) │
|
||
│ ┌──────────────────────┐ ┌───────────────────────┐ │
|
||
│ │ AuthContext │ │ AuthModalContext │ │
|
||
│ │ - user │ │ - isAuthModalOpen │ │
|
||
│ │ - isAuthenticated │ │ - openAuthModal() │ │
|
||
│ │ - checkSession() │ │ - handleLoginSuccess()│ │
|
||
│ └──────────────────────┘ └───────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 业务逻辑层 (Components) │
|
||
│ ┌──────────────────┐ ┌─────────────┐ ┌────────────┐ │
|
||
│ │ AuthFormContent │ │WechatRegister│ │VerifyCodeInput│ │
|
||
│ │ (认证表单) │ │ (微信扫码) │ │ (验证码输入)│ │
|
||
│ └──────────────────┘ └─────────────┘ └────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 服务层 (API Service) │
|
||
│ ┌──────────────┐ │
|
||
│ │ authService │ │
|
||
│ │ - getWechatQRCode() │
|
||
│ │ - checkWechatStatus() │
|
||
│ │ - loginWithWechat() │
|
||
│ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 后端 API (Flask) │
|
||
│ ┌─────────────────────────────────────────────────┐ │
|
||
│ │ /api/auth/session - Session检查 │ │
|
||
│ │ /api/auth/send-verification-code - 发送验证码 │ │
|
||
│ │ /api/auth/login-with-code - 验证码登录 │ │
|
||
│ │ /api/auth/wechat/qrcode - 获取微信二维码 │ │
|
||
│ │ /api/auth/wechat/check - 检查扫码状态 │ │
|
||
│ │ /api/auth/login/wechat - 微信登录 │ │
|
||
│ │ /api/auth/wechat/h5-auth - 微信H5授权 │ │
|
||
│ │ /api/auth/wechat/h5-callback - 微信H5回调 │ │
|
||
│ │ /api/auth/logout - 退出登录 │ │
|
||
│ └─────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 数据存储层 (Database) │
|
||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||
│ │ User表 │ │ Session存储 │ │ 验证码缓存 │ │
|
||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 认证流程详解
|
||
|
||
### 2.1 手机验证码登录
|
||
|
||
**适用场景**: PC端和移动端通用登录方式
|
||
|
||
#### 流程图
|
||
|
||
```
|
||
用户点击"登录/注册"按钮
|
||
↓
|
||
打开 AuthModal 弹窗
|
||
↓
|
||
输入手机号 → 点击"获取验证码"
|
||
↓
|
||
前端: AuthFormContent.sendVerificationCode()
|
||
↓
|
||
POST /api/auth/send-verification-code
|
||
Body: {
|
||
credential: "13800138000",
|
||
type: "phone",
|
||
purpose: "login"
|
||
}
|
||
↓
|
||
后端: 生成6位验证码 → 发送短信 → 存入Session/Redis (5分钟有效期)
|
||
↓
|
||
返回: { success: true }
|
||
↓
|
||
前端显示: "验证码已发送" + 60秒倒计时
|
||
↓
|
||
用户输入验证码 → 点击"登录/注册"
|
||
↓
|
||
前端: AuthFormContent.handleSubmit()
|
||
↓
|
||
POST /api/auth/login-with-code
|
||
Body: {
|
||
credential: "13800138000",
|
||
verification_code: "123456",
|
||
login_type: "phone"
|
||
}
|
||
↓
|
||
后端验证:
|
||
1. 验证码是否存在 ✓
|
||
2. 验证码是否过期 ✓
|
||
3. 验证码是否匹配 ✓
|
||
4. 查询用户是否存在
|
||
- 存在: 登录 (isNewUser: false)
|
||
- 不存在: 自动注册 (isNewUser: true)
|
||
↓
|
||
设置 Session Cookie (HttpOnly)
|
||
↓
|
||
返回: {
|
||
success: true,
|
||
isNewUser: true/false,
|
||
user: { id, phone, nickname, ... }
|
||
}
|
||
↓
|
||
前端:
|
||
1. checkSession() 更新全局状态
|
||
2. 显示成功提示
|
||
3. 如果 isNewUser=true → 显示昵称设置引导
|
||
4. 关闭弹窗,留在当前页面
|
||
↓
|
||
登录完成 ✅
|
||
```
|
||
|
||
#### 关键代码位置
|
||
|
||
**前端**:
|
||
- `src/components/Auth/AuthFormContent.js:130` - 发送验证码
|
||
- `src/components/Auth/AuthFormContent.js:207` - 提交登录
|
||
- `src/components/Auth/VerificationCodeInput.js` - 验证码输入组件
|
||
|
||
**后端**:
|
||
- `app.py:1826` - POST /api/auth/send-verification-code
|
||
- `app.py:1884` - POST /api/auth/login-with-code
|
||
|
||
---
|
||
|
||
### 2.2 微信PC扫码登录
|
||
|
||
**适用场景**: PC端桌面浏览器
|
||
|
||
#### 流程图
|
||
|
||
```
|
||
用户打开登录弹窗 (桌面端)
|
||
↓
|
||
右侧显示微信二维码区域 (WechatRegister组件)
|
||
↓
|
||
初始状态: 灰色二维码图标 + "获取二维码"按钮
|
||
↓
|
||
用户点击"获取二维码"
|
||
↓
|
||
前端: WechatRegister.getWechatQRCode()
|
||
↓
|
||
GET /api/auth/wechat/qrcode
|
||
↓
|
||
后端:
|
||
1. 生成唯一 session_id (UUID)
|
||
2. 构建微信开放平台授权URL
|
||
3. 存储到临时状态 (5分钟有效期)
|
||
↓
|
||
返回: {
|
||
code: 0,
|
||
data: {
|
||
auth_url: "https://open.weixin.qq.com/connect/qrconnect?...",
|
||
session_id: "uuid-xxxxx"
|
||
}
|
||
}
|
||
↓
|
||
前端:
|
||
1. 在 iframe 中显示微信二维码
|
||
2. 启动轮询: 每2秒检查扫码状态
|
||
3. 启动备用轮询: 每3秒检查 (防止丢失)
|
||
4. 设置超时: 5分钟后二维码过期
|
||
↓
|
||
【轮询检查】
|
||
POST /api/auth/wechat/check
|
||
Body: { session_id: "uuid-xxxxx" }
|
||
↓
|
||
后端返回状态:
|
||
- waiting: 等待扫码
|
||
- scanned: 已扫码,等待确认
|
||
- authorized: 已授权
|
||
- login_success: 登录成功 (老用户)
|
||
- register_success: 注册成功 (新用户)
|
||
- expired: 二维码过期
|
||
↓
|
||
如果状态 = login_success / register_success:
|
||
↓
|
||
前端: WechatRegister.handleLoginSuccess()
|
||
↓
|
||
POST /api/auth/login/wechat
|
||
Body: { session_id: "uuid-xxxxx" }
|
||
↓
|
||
后端:
|
||
1. 从临时状态获取微信用户信息
|
||
2. 查询数据库是否存在该微信用户
|
||
- 存在: 登录
|
||
- 不存在: 自动注册
|
||
3. 设置 Session Cookie
|
||
↓
|
||
返回: {
|
||
success: true,
|
||
user: { id, nickname, avatar_url, ... },
|
||
token: "optional-token"
|
||
}
|
||
↓
|
||
前端:
|
||
1. 停止轮询
|
||
2. checkSession() 更新状态
|
||
3. 显示成功提示
|
||
4. 1秒后跳转 /home
|
||
↓
|
||
登录完成 ✅
|
||
```
|
||
|
||
#### 关键代码位置
|
||
|
||
**前端**:
|
||
- `src/components/Auth/WechatRegister.js:199` - 获取二维码
|
||
- `src/components/Auth/WechatRegister.js:120` - 检查扫码状态
|
||
- `src/components/Auth/WechatRegister.js:85` - 登录成功处理
|
||
- `src/services/authService.js:69` - API服务
|
||
|
||
**后端**:
|
||
- `app.py:2487` - GET /api/auth/wechat/qrcode
|
||
- `app.py:2560` - POST /api/auth/wechat/check
|
||
- `app.py:2743` - POST /api/auth/login/wechat
|
||
|
||
---
|
||
|
||
### 2.3 微信H5网页授权
|
||
|
||
**适用场景**: 移动端浏览器中打开
|
||
|
||
#### 流程图
|
||
|
||
```
|
||
移动端用户点击验证码输入框下方的微信图标
|
||
↓
|
||
前端: AuthFormContent.handleWechatH5Login()
|
||
↓
|
||
构建回调URL: https://yourdomain.com/home/wechat-callback
|
||
↓
|
||
POST /api/auth/wechat/h5-auth
|
||
Body: { redirect_url: "https://..." }
|
||
↓
|
||
后端:
|
||
1. 构建微信网页授权URL (snsapi_userinfo)
|
||
2. 生成 state 参数防止CSRF
|
||
↓
|
||
返回: {
|
||
auth_url: "https://open.weixin.qq.com/connect/oauth2/authorize?..."
|
||
}
|
||
↓
|
||
前端: 延迟500ms后跳转到微信授权页面
|
||
↓
|
||
window.location.href = auth_url
|
||
↓
|
||
【用户在微信授权页面确认】
|
||
↓
|
||
微信回调: https://yourdomain.com/home/wechat-callback?code=xxx&state=yyy
|
||
↓
|
||
前端: WechatCallback 组件接收回调
|
||
↓
|
||
POST /api/auth/wechat/h5-callback
|
||
Body: { code: "xxx", state: "yyy" }
|
||
↓
|
||
后端:
|
||
1. 验证 state 参数
|
||
2. 使用 code 换取 access_token
|
||
3. 使用 access_token 获取微信用户信息
|
||
4. 查询数据库
|
||
- 存在: 登录
|
||
- 不存在: 自动注册
|
||
5. 设置 Session Cookie
|
||
↓
|
||
返回: {
|
||
success: true,
|
||
user: { ... },
|
||
token: "optional"
|
||
}
|
||
↓
|
||
前端:
|
||
1. 存储 token (可选)
|
||
2. checkSession() 更新状态
|
||
3. 显示"登录成功"
|
||
4. 1.5秒后跳转 /home
|
||
↓
|
||
登录完成 ✅
|
||
```
|
||
|
||
#### 关键代码位置
|
||
|
||
**前端**:
|
||
- `src/components/Auth/AuthFormContent.js:308` - 发起H5授权
|
||
- `src/views/Pages/WechatCallback.js:34` - 处理回调
|
||
- `src/services/authService.js:78` - H5授权API
|
||
- `src/services/authService.js:91` - H5回调API
|
||
|
||
**后端**:
|
||
- `app.py:2487+` - POST /api/auth/wechat/h5-auth (需确认)
|
||
- `app.py:2610+` - POST /api/auth/wechat/h5-callback (需确认)
|
||
|
||
---
|
||
|
||
## 3. 路由配置与跳转逻辑
|
||
|
||
### 3.1 路由结构
|
||
|
||
```javascript
|
||
// src/App.js
|
||
<BrowserRouter>
|
||
<Routes>
|
||
{/* 公开路由 - 无需登录 */}
|
||
<Route path="/home/*" element={<Home />} />
|
||
<Route path="/auth/*" element={<Auth />} />
|
||
|
||
{/* 受保护路由 - 需要登录 */}
|
||
<Route path="/admin/*" element={<Admin />} />
|
||
</Routes>
|
||
</BrowserRouter>
|
||
|
||
// src/layouts/Home.js (公开路由)
|
||
<Routes>
|
||
<Route path="/" element={<HomePage />} />
|
||
<Route path="/wechat-callback" element={<WechatCallback />} />
|
||
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
||
<Route path="/user-agreement" element={<UserAgreement />} />
|
||
|
||
{/* 需要登录的页面 */}
|
||
<Route path="/profile" element={
|
||
<ProtectedRoute>
|
||
<ProfilePage />
|
||
</ProtectedRoute>
|
||
} />
|
||
<Route path="/center" element={
|
||
<ProtectedRoute>
|
||
<CenterDashboard />
|
||
</ProtectedRoute>
|
||
} />
|
||
</Routes>
|
||
```
|
||
|
||
### 3.2 路由保护机制
|
||
|
||
**ProtectedRoute 组件** (`src/components/ProtectedRoute.js`)
|
||
|
||
```javascript
|
||
const ProtectedRoute = ({ children }) => {
|
||
const { isAuthenticated, isLoading, user } = useAuth();
|
||
const { openAuthModal, isAuthModalOpen } = useAuthModal();
|
||
|
||
// 未登录时自动弹出认证窗口
|
||
useEffect(() => {
|
||
if (!isLoading && !isAuthenticated && !user && !isAuthModalOpen) {
|
||
openAuthModal(currentPath); // 记录当前路径
|
||
}
|
||
}, [isAuthenticated, user, isLoading]);
|
||
|
||
// 加载中: 显示 Spinner
|
||
if (isLoading) {
|
||
return <LoadingScreen />;
|
||
}
|
||
|
||
// 未登录: 显示页面 + 自动打开弹窗 (非阻断式)
|
||
// 已登录: 正常显示页面
|
||
return children;
|
||
};
|
||
```
|
||
|
||
**特点**:
|
||
- ✅ 非阻断式保护 (弹窗而非重定向)
|
||
- ✅ 记录原始路径 (登录后可返回)
|
||
- ✅ 避免白屏 (先显示页面骨架)
|
||
|
||
### 3.3 跳转逻辑总结
|
||
|
||
| 场景 | 触发位置 | 跳转目标 | 说明 |
|
||
|------|---------|---------|------|
|
||
| 未登录访问受保护页面 | ProtectedRoute | 留在当前页 + 弹窗 | 非阻断式 |
|
||
| 点击导航栏登录按钮 | HomeNavbar:740 | 打开 AuthModal | 弹窗形式 |
|
||
| 手机验证码登录成功 | AuthFormContent:284 | 留在当前页 | 关闭弹窗即可 |
|
||
| 微信扫码登录成功 | WechatRegister:106 | 跳转 /home | 1秒延迟 |
|
||
| 微信H5授权成功 | WechatCallback:69 | 跳转 /home | 1.5秒延迟 |
|
||
| 用户点击退出登录 | HomeNavbar:227 | 跳转 /home | 清除Session |
|
||
| 关闭弹窗未登录 | AuthModalContext:58 | 跳转 /home | 防止停留受保护页 |
|
||
|
||
---
|
||
|
||
## 4. API接口文档
|
||
|
||
### 4.1 Session检查
|
||
|
||
**接口**: `GET /api/auth/session`
|
||
|
||
**用途**: 检查当前用户登录状态
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/auth/session HTTP/1.1
|
||
Cookie: session=xxx
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"isAuthenticated": true,
|
||
"user": {
|
||
"id": 123,
|
||
"phone": "13800138000",
|
||
"nickname": "价小前用户",
|
||
"email": "user@example.com",
|
||
"avatar_url": "https://...",
|
||
"has_wechat": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/contexts/AuthContext.js:85
|
||
const checkSession = async () => {
|
||
const response = await fetch('/api/auth/session', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
if (data.isAuthenticated) {
|
||
setUser(data.user);
|
||
setIsAuthenticated(true);
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
### 4.2 发送验证码
|
||
|
||
**接口**: `POST /api/auth/send-verification-code`
|
||
|
||
**用途**: 发送手机验证码
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/send-verification-code HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "验证码已发送"
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "手机号格式不正确"
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/components/Auth/AuthFormContent.js:153
|
||
const response = await fetch('/api/auth/send-verification-code', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
credential: phone,
|
||
type: 'phone',
|
||
purpose: 'login'
|
||
})
|
||
});
|
||
```
|
||
|
||
**限流规则**:
|
||
- 同一手机号: 60秒内只能发送1次
|
||
- 验证码有效期: 5分钟
|
||
|
||
---
|
||
|
||
### 4.3 验证码登录
|
||
|
||
**接口**: `POST /api/auth/login-with-code`
|
||
|
||
**用途**: 使用验证码登录/注册
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/login-with-code HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
```
|
||
|
||
**响应 (登录成功)**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"isNewUser": false,
|
||
"user": {
|
||
"id": 123,
|
||
"phone": "13800138000",
|
||
"nickname": "价小前用户"
|
||
}
|
||
}
|
||
```
|
||
|
||
**响应 (注册成功)**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"isNewUser": true,
|
||
"user": {
|
||
"id": 124,
|
||
"phone": "13900139000",
|
||
"nickname": "用户13900139000"
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "验证码已过期或不存在"
|
||
}
|
||
```
|
||
|
||
**自动注册逻辑**:
|
||
- 如果手机号不存在 → 自动创建新用户
|
||
- 默认昵称: `用户{手机号}`
|
||
- isNewUser 标记用于前端引导设置昵称
|
||
|
||
---
|
||
|
||
### 4.4 获取微信二维码
|
||
|
||
**接口**: `GET /api/auth/wechat/qrcode`
|
||
|
||
**用途**: 获取微信PC扫码登录二维码
|
||
|
||
**请求**:
|
||
```http
|
||
GET /api/auth/wechat/qrcode HTTP/1.1
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"auth_url": "https://open.weixin.qq.com/connect/qrconnect?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_login&state=yyy#wechat_redirect",
|
||
"session_id": "550e8400-e29b-41d4-a716-446655440000"
|
||
},
|
||
"message": "success"
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/services/authService.js:69
|
||
getWechatQRCode: async () => {
|
||
return await apiRequest('/api/auth/wechat/qrcode');
|
||
}
|
||
```
|
||
|
||
**二维码有效期**: 5分钟
|
||
|
||
---
|
||
|
||
### 4.5 检查微信扫码状态
|
||
|
||
**接口**: `POST /api/auth/wechat/check`
|
||
|
||
**用途**: 轮询检查微信扫码状态
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/wechat/check HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
```
|
||
|
||
**响应状态**:
|
||
|
||
| status | 说明 | 前端行为 |
|
||
|--------|------|---------|
|
||
| waiting | 等待扫码 | 继续轮询 |
|
||
| scanned | 已扫码,等待确认 | 显示提示 + 继续轮询 |
|
||
| authorized | 已授权 | 继续轮询 |
|
||
| login_success | 登录成功 (老用户) | 停止轮询 + 调用登录接口 |
|
||
| register_success | 注册成功 (新用户) | 停止轮询 + 调用登录接口 |
|
||
| expired | 二维码过期 | 停止轮询 + 显示刷新按钮 |
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"status": "login_success",
|
||
"user_info": {
|
||
"openid": "xxx",
|
||
"nickname": "微信用户",
|
||
"avatar_url": "https://..."
|
||
}
|
||
}
|
||
```
|
||
|
||
**前端轮询**:
|
||
```javascript
|
||
// src/components/Auth/WechatRegister.js:180
|
||
pollIntervalRef.current = setInterval(() => {
|
||
checkWechatStatus();
|
||
}, 2000); // 每2秒检查一次
|
||
```
|
||
|
||
---
|
||
|
||
### 4.6 微信登录
|
||
|
||
**接口**: `POST /api/auth/login/wechat`
|
||
|
||
**用途**: 使用微信 session_id 完成登录
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/login/wechat HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"user": {
|
||
"id": 123,
|
||
"nickname": "微信用户",
|
||
"avatar_url": "https://...",
|
||
"has_wechat": true
|
||
},
|
||
"token": "optional-token"
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/components/Auth/WechatRegister.js:87
|
||
const response = await authService.loginWithWechat(sessionId);
|
||
if (response?.success) {
|
||
// 存储用户信息
|
||
if (response.user) {
|
||
localStorage.setItem('user', JSON.stringify(response.user));
|
||
}
|
||
// 跳转首页
|
||
navigate('/home');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.7 微信H5授权
|
||
|
||
**接口**: `POST /api/auth/wechat/h5-auth`
|
||
|
||
**用途**: 获取微信H5网页授权链接
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/wechat/h5-auth HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"auth_url": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_userinfo&state=yyy#wechat_redirect"
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/components/Auth/AuthFormContent.js:323
|
||
const response = await authService.getWechatH5AuthUrl(redirectUrl);
|
||
if (response?.auth_url) {
|
||
window.location.href = response.auth_url;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.8 微信H5回调
|
||
|
||
**接口**: `POST /api/auth/wechat/h5-callback`
|
||
|
||
**用途**: 处理微信H5授权回调
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/wechat/h5-callback HTTP/1.1
|
||
Content-Type: application/json
|
||
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"user": {
|
||
"id": 123,
|
||
"nickname": "微信用户",
|
||
"avatar_url": "https://..."
|
||
},
|
||
"token": "optional-token"
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/views/Pages/WechatCallback.js:46
|
||
const response = await authService.handleWechatH5Callback(code, state);
|
||
if (response?.success) {
|
||
await checkSession();
|
||
navigate('/home');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.9 退出登录
|
||
|
||
**接口**: `POST /api/auth/logout`
|
||
|
||
**用途**: 清除用户Session
|
||
|
||
**请求**:
|
||
```http
|
||
POST /api/auth/logout HTTP/1.1
|
||
Cookie: session=xxx
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "已退出登录"
|
||
}
|
||
```
|
||
|
||
**前端调用**:
|
||
```javascript
|
||
// src/contexts/AuthContext.js:120
|
||
const logout = async () => {
|
||
await fetch('/api/auth/logout', {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
});
|
||
setUser(null);
|
||
setIsAuthenticated(false);
|
||
navigate('/home');
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 前端组件架构
|
||
|
||
### 5.1 核心组件关系图
|
||
|
||
```
|
||
App.js (根组件)
|
||
└── AuthProvider (全局认证状态)
|
||
└── AuthModalProvider (弹窗管理)
|
||
├── HomeNavbar (导航栏)
|
||
│ └── Button onClick={openAuthModal} (登录按钮)
|
||
│
|
||
├── AuthModalManager (弹窗管理器)
|
||
│ └── Modal
|
||
│ └── AuthFormContent (认证表单)
|
||
│ ├── VerificationCodeInput (验证码输入)
|
||
│ └── WechatRegister (微信二维码)
|
||
│
|
||
└── ProtectedRoute (路由保护)
|
||
└── 受保护的页面组件
|
||
```
|
||
|
||
### 5.2 组件详解
|
||
|
||
#### AuthContext (`src/contexts/AuthContext.js`)
|
||
|
||
**职责**: 全局认证状态管理
|
||
|
||
**状态**:
|
||
```javascript
|
||
{
|
||
user: {
|
||
id: 123,
|
||
phone: "13800138000",
|
||
nickname: "价小前用户",
|
||
email: "user@example.com",
|
||
avatar_url: "https://...",
|
||
has_wechat: true
|
||
},
|
||
isAuthenticated: true,
|
||
isLoading: false
|
||
}
|
||
```
|
||
|
||
**方法**:
|
||
- `checkSession()` - 检查当前登录状态
|
||
- `logout()` - 退出登录
|
||
|
||
**使用**:
|
||
```javascript
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
|
||
const { user, isAuthenticated, isLoading, checkSession, logout } = useAuth();
|
||
```
|
||
|
||
---
|
||
|
||
#### AuthModalContext (`src/contexts/AuthModalContext.js`)
|
||
|
||
**职责**: 认证弹窗状态管理
|
||
|
||
**状态**:
|
||
```javascript
|
||
{
|
||
isAuthModalOpen: false,
|
||
redirectUrl: null, // 登录成功后跳转的URL (可选)
|
||
}
|
||
```
|
||
|
||
**方法**:
|
||
- `openAuthModal(url, callback)` - 打开认证弹窗
|
||
- `closeModal()` - 关闭弹窗 (未登录则跳转首页)
|
||
- `handleLoginSuccess(user)` - 登录成功处理
|
||
|
||
**使用**:
|
||
```javascript
|
||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||
|
||
const { isAuthModalOpen, openAuthModal, handleLoginSuccess } = useAuthModal();
|
||
|
||
// 打开弹窗
|
||
openAuthModal();
|
||
|
||
// 登录成功
|
||
handleLoginSuccess(userData);
|
||
```
|
||
|
||
---
|
||
|
||
#### AuthModalManager (`src/components/Auth/AuthModalManager.js`)
|
||
|
||
**职责**: 渲染认证弹窗 UI
|
||
|
||
**特点**:
|
||
- 响应式尺寸 (移动端 md, 桌面端 xl)
|
||
- 条件渲染 (仅在打开时渲染,避免不必要的Portal)
|
||
- 半透明背景 + 模糊效果
|
||
- 禁止点击背景关闭 (防止误操作)
|
||
|
||
**响应式配置**:
|
||
```javascript
|
||
const modalSize = useBreakpointValue({
|
||
base: "md", // 移动端
|
||
md: "lg", // 中屏
|
||
lg: "xl" // 大屏
|
||
});
|
||
|
||
const modalMaxW = useBreakpointValue({
|
||
base: "90%", // 移动端占90%宽度
|
||
md: "700px" // 桌面端固定700px
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
#### AuthFormContent (`src/components/Auth/AuthFormContent.js`)
|
||
|
||
**职责**: 认证表单主体内容
|
||
|
||
**功能**:
|
||
1. 手机号输入
|
||
2. 验证码输入 + 发送验证码
|
||
3. 登录/注册按钮
|
||
4. 微信扫码 (桌面端右侧)
|
||
5. 微信H5登录 (移动端验证码下方图标)
|
||
6. 隐私协议链接
|
||
|
||
**布局**:
|
||
```
|
||
桌面端 (Desktop):
|
||
┌─────────────────────────────────────┐
|
||
│ 价值前沿 - 开启您的投资之旅 │
|
||
├──────────────────┬──────────────────┤
|
||
│ 登陆/注册 │ 微信扫码 │
|
||
│ │ │
|
||
│ 手机号输入框 │ [二维码区域] │
|
||
│ 验证码输入框 │ │
|
||
│ [获取验证码] │ 请使用微信扫码 │
|
||
│ │ │
|
||
│ [登录/注册按钮] │ │
|
||
│ │ │
|
||
│ 《隐私政策》 │ │
|
||
└──────────────────┴──────────────────┘
|
||
|
||
移动端 (Mobile):
|
||
┌─────────────────────┐
|
||
│ 价值前沿 │
|
||
│ 开启您的投资之旅 │
|
||
├─────────────────────┤
|
||
│ 登陆/注册 │
|
||
│ │
|
||
│ 手机号输入框 │
|
||
│ │
|
||
│ 验证码输入框 │
|
||
│ [获取验证码] │
|
||
│ 其他登录方式: [微信] │
|
||
│ │
|
||
│ [登录/注册按钮] │
|
||
│ │
|
||
│ 《隐私政策》 │
|
||
└─────────────────────┘
|
||
```
|
||
|
||
**关键逻辑**:
|
||
```javascript
|
||
// 发送验证码 (60秒倒计时)
|
||
const sendVerificationCode = async () => {
|
||
const response = await fetch('/api/auth/send-verification-code', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
credential: phone,
|
||
type: 'phone',
|
||
purpose: 'login'
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
setCountdown(60); // 启动60秒倒计时
|
||
}
|
||
};
|
||
|
||
// 提交登录
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
const response = await fetch('/api/auth/login-with-code', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
credential: phone,
|
||
verification_code: verificationCode,
|
||
login_type: 'phone'
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
await checkSession(); // 更新全局状态
|
||
|
||
if (data.isNewUser) {
|
||
// 新用户引导设置昵称
|
||
setShowNicknamePrompt(true);
|
||
} else {
|
||
handleLoginSuccess(); // 关闭弹窗
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
#### WechatRegister (`src/components/Auth/WechatRegister.js`)
|
||
|
||
**职责**: 微信PC扫码登录组件
|
||
|
||
**状态机**:
|
||
```
|
||
NONE (初始) → 点击"获取二维码"
|
||
↓
|
||
WAITING (显示二维码 + 轮询)
|
||
↓
|
||
SCANNED (提示"请在手机上确认")
|
||
↓
|
||
LOGIN_SUCCESS / REGISTER_SUCCESS
|
||
↓
|
||
调用登录接口 → 跳转首页
|
||
|
||
EXPIRED (二维码过期 → 显示"点击刷新")
|
||
```
|
||
|
||
**轮询机制**:
|
||
```javascript
|
||
// 主轮询: 每2秒检查
|
||
pollIntervalRef.current = setInterval(() => {
|
||
checkWechatStatus();
|
||
}, 2000);
|
||
|
||
// 备用轮询: 每3秒检查 (防止主轮询失败)
|
||
backupPollIntervalRef.current = setInterval(() => {
|
||
checkWechatStatus().catch(error => {
|
||
console.warn('备用轮询检查失败(静默处理):', error);
|
||
});
|
||
}, 3000);
|
||
|
||
// 超时机制: 5分钟后停止轮询
|
||
timeoutRef.current = setTimeout(() => {
|
||
clearTimers();
|
||
setWechatStatus(WECHAT_STATUS.EXPIRED);
|
||
}, 300000);
|
||
```
|
||
|
||
**二维码缩放**:
|
||
```javascript
|
||
// 使用 ResizeObserver 监听容器尺寸变化
|
||
const calculateScale = () => {
|
||
const { width, height } = containerRef.current.getBoundingClientRect();
|
||
|
||
const scaleX = width / 300; // 二维码原始宽度300px
|
||
const scaleY = height / 350; // 二维码原始高度350px
|
||
const newScale = Math.min(scaleX, scaleY, 1.0);
|
||
|
||
setScale(Math.max(newScale, 0.3)); // 最小0.3,最大1.0
|
||
};
|
||
|
||
// iframe 缩放
|
||
<iframe
|
||
src={wechatAuthUrl}
|
||
width="300"
|
||
height="350"
|
||
style={{
|
||
transform: `scale(${scale})`,
|
||
transformOrigin: 'center center'
|
||
}}
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
#### VerificationCodeInput (`src/components/Auth/VerificationCodeInput.js`)
|
||
|
||
**职责**: 通用验证码输入组件
|
||
|
||
**Props**:
|
||
```javascript
|
||
{
|
||
value: string, // 验证码值
|
||
onChange: function, // 输入变化回调
|
||
onSendCode: function, // 发送验证码回调
|
||
countdown: number, // 倒计时秒数
|
||
isLoading: boolean, // 表单加载状态
|
||
isSending: boolean, // 发送中状态
|
||
error: string, // 错误信息
|
||
placeholder: string, // 输入框提示
|
||
buttonText: string, // 按钮文本
|
||
countdownText: function, // 倒计时文本生成器
|
||
colorScheme: string, // 按钮颜色
|
||
isRequired: boolean // 是否必填
|
||
}
|
||
```
|
||
|
||
**错误处理包装器**:
|
||
```javascript
|
||
// 确保所有错误都被捕获,防止被 ErrorBoundary 捕获
|
||
const handleSendCode = async () => {
|
||
try {
|
||
if (onSendCode) {
|
||
await onSendCode();
|
||
}
|
||
} catch (error) {
|
||
// 错误已经在父组件处理,这里只需要防止未捕获的 Promise rejection
|
||
console.error('Send code error (caught in VerificationCodeInput):', error);
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
#### ProtectedRoute (`src/components/ProtectedRoute.js`)
|
||
|
||
**职责**: 路由保护组件
|
||
|
||
**保护策略**:
|
||
```javascript
|
||
const ProtectedRoute = ({ children }) => {
|
||
const { isAuthenticated, isLoading, user } = useAuth();
|
||
const { openAuthModal, isAuthModalOpen } = useAuthModal();
|
||
|
||
const currentPath = window.location.pathname;
|
||
|
||
// 未登录时自动弹出认证窗口
|
||
useEffect(() => {
|
||
if (!isLoading && !isAuthenticated && !user && !isAuthModalOpen) {
|
||
openAuthModal(currentPath); // 记录当前路径
|
||
}
|
||
}, [isAuthenticated, user, isLoading]);
|
||
|
||
// 加载中: 显示 Spinner
|
||
if (isLoading) {
|
||
return <LoadingScreen />;
|
||
}
|
||
|
||
// 未登录或已登录: 都渲染子组件 (弹窗由 useEffect 触发)
|
||
return children;
|
||
};
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 非阻断式 (不跳转,只弹窗)
|
||
- ✅ 用户体验好 (可以看到页面内容)
|
||
- ✅ 记录原始路径 (登录后可返回)
|
||
|
||
---
|
||
|
||
#### HomeNavbar (`src/components/Navbars/HomeNavbar.js`)
|
||
|
||
**职责**: 顶部导航栏
|
||
|
||
**未登录状态** (`src/components/Navbars/HomeNavbar.js:735`):
|
||
```javascript
|
||
<Button
|
||
colorScheme="blue"
|
||
variant="solid"
|
||
size="sm"
|
||
onClick={() => openAuthModal()}
|
||
>
|
||
登录 / 注册
|
||
</Button>
|
||
```
|
||
|
||
**已登录状态** (`src/components/Navbars/HomeNavbar.js:523`):
|
||
```javascript
|
||
<HStack spacing={3}>
|
||
{/* 用户菜单 */}
|
||
<Menu>
|
||
<MenuButton as={Button} leftIcon={<Avatar />}>
|
||
{getDisplayName()}
|
||
</MenuButton>
|
||
<MenuList>
|
||
<MenuItem onClick={() => navigate('/home/profile')}>
|
||
👤 个人资料
|
||
</MenuItem>
|
||
<MenuItem onClick={handleLogout} color="red.500">
|
||
🚪 退出登录
|
||
</MenuItem>
|
||
</MenuList>
|
||
</Menu>
|
||
|
||
{/* 自选股 */}
|
||
<Menu onOpen={loadWatchlistQuotes}>
|
||
<MenuButton as={Button} leftIcon={<FiStar />}>
|
||
自选股
|
||
</MenuButton>
|
||
{/* ... */}
|
||
</Menu>
|
||
|
||
{/* 自选事件 */}
|
||
<Menu onOpen={loadFollowingEvents}>
|
||
<MenuButton as={Button} leftIcon={<FiCalendar />}>
|
||
自选事件
|
||
</MenuButton>
|
||
{/* ... */}
|
||
</Menu>
|
||
</HStack>
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 状态管理机制
|
||
|
||
### 6.1 认证状态流转
|
||
|
||
```
|
||
应用启动
|
||
↓
|
||
AuthContext 初始化
|
||
↓
|
||
useEffect(() => { checkSession() }, [])
|
||
↓
|
||
GET /api/auth/session
|
||
↓
|
||
┌─────────────────────────────────────┐
|
||
│ 响应: { isAuthenticated: true } │
|
||
│ ↓ │
|
||
│ setUser(data.user) │
|
||
│ setIsAuthenticated(true) │
|
||
│ isLoading = false │
|
||
└─────────────────────────────────────┘
|
||
↓
|
||
全局状态更新完成
|
||
↓
|
||
所有使用 useAuth() 的组件重新渲染
|
||
↓
|
||
- HomeNavbar 显示用户信息
|
||
- ProtectedRoute 放行受保护页面
|
||
- AuthModal 不自动弹出
|
||
```
|
||
|
||
### 6.2 状态持久化机制
|
||
|
||
**Session-based Authentication**:
|
||
```
|
||
登录成功
|
||
↓
|
||
后端设置 HttpOnly Cookie
|
||
Set-Cookie: session=xxx; HttpOnly; SameSite=Lax; Path=/
|
||
↓
|
||
浏览器自动存储 Cookie
|
||
↓
|
||
后续所有请求自动携带 Cookie (credentials: 'include')
|
||
↓
|
||
后端验证 Cookie → 识别用户身份
|
||
```
|
||
|
||
**刷新页面**:
|
||
```
|
||
用户刷新页面
|
||
↓
|
||
React App 重新挂载
|
||
↓
|
||
AuthContext useEffect 触发
|
||
↓
|
||
checkSession() 调用 /api/auth/session
|
||
↓
|
||
请求自动携带 session cookie
|
||
↓
|
||
后端验证 cookie → 返回用户信息
|
||
↓
|
||
前端恢复登录状态 ✅
|
||
```
|
||
|
||
**跨标签页同步** (当前未实现):
|
||
```javascript
|
||
// 建议实现方式
|
||
useEffect(() => {
|
||
const handleStorageChange = (e) => {
|
||
if (e.key === 'auth_sync') {
|
||
checkSession(); // 重新检查登录状态
|
||
}
|
||
};
|
||
|
||
window.addEventListener('storage', handleStorageChange);
|
||
return () => window.removeEventListener('storage', handleStorageChange);
|
||
}, []);
|
||
|
||
// 登录成功时触发同步
|
||
localStorage.setItem('auth_sync', Date.now());
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Session持久化
|
||
|
||
### 7.1 Cookie配置
|
||
|
||
**后端配置** (Flask):
|
||
```python
|
||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||
app.config['SESSION_COOKIE_SECURE'] = True # 生产环境 HTTPS
|
||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
||
```
|
||
|
||
**前端配置** (fetch):
|
||
```javascript
|
||
fetch('/api/auth/session', {
|
||
credentials: 'include' // ✅ 必须包含,否则 cookie 不会发送
|
||
});
|
||
```
|
||
|
||
### 7.2 跨域处理
|
||
|
||
**开发环境** (`craco.config.js`):
|
||
```javascript
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://49.232.185.254:5001',
|
||
changeOrigin: true,
|
||
secure: false,
|
||
logLevel: 'debug'
|
||
}
|
||
}
|
||
```
|
||
|
||
**生产环境**:
|
||
- 前后端同域: 无需额外配置
|
||
- 前后端跨域:
|
||
```python
|
||
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
||
app.config['SESSION_COOKIE_SECURE'] = True # 必须 HTTPS
|
||
|
||
# CORS 配置
|
||
CORS(app, supports_credentials=True, origins=['https://yourdomain.com'])
|
||
```
|
||
|
||
### 7.3 Session有效期
|
||
|
||
**默认配置**:
|
||
- 有效期: 7天 (可配置)
|
||
- 自动续期: 每次请求后重置过期时间
|
||
- 过期处理: 前端收到 401 → 调用 `checkSession()` → 自动弹出登录窗口
|
||
|
||
**Session 超时处理**:
|
||
```javascript
|
||
// 在关键 API 调用中捕获 401 错误
|
||
const apiRequest = async (url, options) => {
|
||
const response = await fetch(url, options);
|
||
|
||
if (response.status === 401) {
|
||
// Session 已过期
|
||
await checkSession(); // 重新检查状态
|
||
// 如果确认未登录,AuthContext 会自动清除状态
|
||
}
|
||
|
||
return response;
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 错误处理策略
|
||
|
||
### 8.1 错误分类
|
||
|
||
根据 [错误处理文档](./ERROR_HANDLING_STRATEGY.md),错误分为4类:
|
||
|
||
| 类别 | 场景 | 处理方式 |
|
||
|-----|------|---------|
|
||
| **A类** | 致命错误 (组件崩溃) | ErrorBoundary 捕获 + 显示错误页面 |
|
||
| **B类** | 用户操作失败 | Toast 提示 + 不中断页面 |
|
||
| **C类** | 数据加载失败 | 优雅降级 + 显示空状态 |
|
||
| **D类** | 后台任务失败 | 静默处理 + 控制台日志 |
|
||
|
||
### 8.2 认证相关错误处理
|
||
|
||
**发送验证码失败** (B类):
|
||
```javascript
|
||
try {
|
||
const response = await fetch('/api/auth/send-verification-code', {...});
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.error || '发送失败');
|
||
}
|
||
|
||
toast({ title: "验证码已发送", status: "success" });
|
||
} catch (error) {
|
||
// ✅ Toast 提示,不中断页面
|
||
toast({
|
||
title: "发送验证码失败",
|
||
description: error.message,
|
||
status: "error"
|
||
});
|
||
}
|
||
```
|
||
|
||
**验证码登录失败** (B类):
|
||
```javascript
|
||
try {
|
||
const response = await fetch('/api/auth/login-with-code', {...});
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json();
|
||
throw new Error(data.error || '登录失败');
|
||
}
|
||
|
||
toast({ title: "登录成功", status: "success" });
|
||
} catch (error) {
|
||
// ✅ Toast 提示
|
||
toast({
|
||
title: "登录失败",
|
||
description: error.message,
|
||
status: "error"
|
||
});
|
||
}
|
||
```
|
||
|
||
**微信轮询失败** (D类):
|
||
```javascript
|
||
// 备用轮询机制: 静默处理错误
|
||
backupPollIntervalRef.current = setInterval(() => {
|
||
try {
|
||
checkWechatStatus().catch(error => {
|
||
// ✅ 静默处理,只记录日志
|
||
console.warn('备用轮询检查失败(静默处理):', error);
|
||
});
|
||
} catch (error) {
|
||
console.warn('备用轮询执行出错(静默处理):', error);
|
||
}
|
||
}, 3000);
|
||
```
|
||
|
||
**按钮点击错误防护**:
|
||
```javascript
|
||
// 防止未捕获的 Promise rejection 被 ErrorBoundary 捕获
|
||
const handleGetQRCodeClick = useCallback(async () => {
|
||
try {
|
||
await getWechatQRCode();
|
||
} catch (error) {
|
||
// ✅ 三层错误防护
|
||
// Layer 1: 业务逻辑 try-catch (显示 toast)
|
||
// Layer 2: 按钮包装器 try-catch (防止 Promise rejection)
|
||
// Layer 3: ErrorBoundary (只捕获致命错误)
|
||
console.error('QR code button click error (caught in handler):', error);
|
||
}
|
||
}, [getWechatQRCode]);
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 安全机制
|
||
|
||
### 9.1 防范措施
|
||
|
||
| 安全威胁 | 防范措施 | 实现位置 |
|
||
|---------|---------|---------|
|
||
| **CSRF 攻击** | SameSite Cookie + CSRF Token | 后端 Flask |
|
||
| **XSS 攻击** | HttpOnly Cookie + CSP | 后端配置 |
|
||
| **会话劫持** | HTTPS Only Cookie (生产) | 后端配置 |
|
||
| **验证码暴力破解** | 60秒限流 + 5分钟过期 | 后端逻辑 |
|
||
| **微信授权劫持** | State 参数验证 | 后端 wechat callback |
|
||
| **敏感信息泄露** | Token 不存 localStorage (可选) | 前端规范 |
|
||
|
||
### 9.2 HttpOnly Cookie
|
||
|
||
**优势**:
|
||
- ✅ JavaScript 无法访问 (防止 XSS 窃取)
|
||
- ✅ 自动随请求发送 (无需手动管理)
|
||
- ✅ 服务器端管理 (更安全)
|
||
|
||
**配置**:
|
||
```python
|
||
# 后端设置
|
||
@app.after_request
|
||
def set_session_cookie(response):
|
||
response.set_cookie(
|
||
'session',
|
||
value=session_id,
|
||
httponly=True, # ✅ JavaScript 无法访问
|
||
secure=True, # ✅ 仅 HTTPS 传输
|
||
samesite='Lax', # ✅ 防止 CSRF
|
||
max_age=7*24*60*60 # ✅ 7天有效期
|
||
)
|
||
return response
|
||
```
|
||
|
||
### 9.3 验证码安全
|
||
|
||
**限流规则**:
|
||
```python
|
||
# 同一手机号60秒内只能发送1次
|
||
rate_limit = {
|
||
'key': f'sms:{phone}',
|
||
'ttl': 60,
|
||
'max_requests': 1
|
||
}
|
||
|
||
# 验证码5分钟有效
|
||
verification_code = {
|
||
'code': '123456',
|
||
'expires_at': datetime.now() + timedelta(minutes=5)
|
||
}
|
||
```
|
||
|
||
**验证逻辑**:
|
||
```python
|
||
def verify_code(phone, code):
|
||
stored = get_verification_code(phone)
|
||
|
||
if not stored:
|
||
raise ValueError("验证码不存在或已过期")
|
||
|
||
if stored['expires_at'] < datetime.now():
|
||
raise ValueError("验证码已过期")
|
||
|
||
if stored['code'] != code:
|
||
raise ValueError("验证码错误")
|
||
|
||
# 验证成功后立即删除 (一次性使用)
|
||
delete_verification_code(phone)
|
||
return True
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 调试指南
|
||
|
||
### 10.1 常见问题排查
|
||
|
||
#### 问题1: 验证码提示"已过期或不存在"
|
||
|
||
**可能原因**:
|
||
1. Session cookie 未正确发送
|
||
2. 前后端跨域配置问题
|
||
3. 验证码确实过期 (>5分钟)
|
||
|
||
**排查步骤**:
|
||
```bash
|
||
# 1. 检查 fetch 请求是否包含 credentials
|
||
console.log('Fetch options:', {
|
||
credentials: 'include' // ✅ 必须包含
|
||
});
|
||
|
||
# 2. 检查浏览器 Network 面板
|
||
- Request Headers 是否包含 Cookie
|
||
- Response Headers 是否包含 Set-Cookie
|
||
|
||
# 3. 检查后端日志
|
||
- Session 是否正确存储验证码
|
||
- 验证码是否已过期
|
||
```
|
||
|
||
**解决方法**:
|
||
```javascript
|
||
// ✅ 确保所有认证相关请求都包含 credentials
|
||
fetch('/api/auth/send-verification-code', {
|
||
method: 'POST',
|
||
credentials: 'include', // 关键!
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({...})
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
#### 问题2: 刷新页面后登录状态丢失
|
||
|
||
**可能原因**:
|
||
1. `checkSession()` 未在 App 启动时调用
|
||
2. Cookie 被浏览器阻止 (隐私模式/第三方 Cookie 限制)
|
||
3. Session 已过期
|
||
|
||
**排查步骤**:
|
||
```javascript
|
||
// 1. 检查 AuthContext useEffect
|
||
useEffect(() => {
|
||
console.log('🔍 App mounted, checking session...');
|
||
checkSession();
|
||
}, []);
|
||
|
||
// 2. 检查 checkSession 响应
|
||
const checkSession = async () => {
|
||
try {
|
||
const response = await fetch('/api/auth/session', {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
console.log('✅ Session check result:', data);
|
||
} catch (error) {
|
||
console.error('❌ Session check failed:', error);
|
||
}
|
||
};
|
||
```
|
||
|
||
**解决方法**:
|
||
- 确认 `AuthContext` 在 `App.js` 根级别
|
||
- 检查浏览器 Cookie 设置 (允许第三方 Cookie)
|
||
- 检查 Session 是否过期 (后端日志)
|
||
|
||
---
|
||
|
||
#### 问题3: 微信扫码后前端未收到状态更新
|
||
|
||
**可能原因**:
|
||
1. 轮询已停止 (组件卸载)
|
||
2. 后端回调未正确更新状态
|
||
3. `session_id` 不匹配
|
||
|
||
**排查步骤**:
|
||
```javascript
|
||
// 1. 检查轮询是否运行
|
||
console.log('⏱️ Polling active:', {
|
||
pollInterval: pollIntervalRef.current,
|
||
backupPoll: backupPollIntervalRef.current,
|
||
timeout: timeoutRef.current
|
||
});
|
||
|
||
// 2. 检查每次轮询响应
|
||
const checkWechatStatus = async () => {
|
||
const response = await authService.checkWechatStatus(sessionId);
|
||
console.log('📊 Wechat status:', response);
|
||
|
||
if (response.status === 'login_success') {
|
||
console.log('✅ Login success detected!');
|
||
}
|
||
};
|
||
|
||
// 3. 检查 session_id 一致性
|
||
console.log('Session ID:', {
|
||
stored: wechatSessionId,
|
||
sent: sessionId
|
||
});
|
||
```
|
||
|
||
**解决方法**:
|
||
- 确认组件未卸载 (`isMountedRef.current === true`)
|
||
- 检查后端微信回调逻辑
|
||
- 查看后端日志确认状态是否更新
|
||
|
||
---
|
||
|
||
#### 问题4: ErrorBoundary 捕获按钮点击错误
|
||
|
||
**可能原因**:
|
||
- async 函数直接传递给 `onClick`,导致未捕获的 Promise rejection
|
||
|
||
**错误代码**:
|
||
```javascript
|
||
// ❌ 错误: 直接传递 async 函数
|
||
<Button onClick={async () => {
|
||
await sendCode(); // 如果失败,会被 ErrorBoundary 捕获
|
||
}}>
|
||
发送验证码
|
||
</Button>
|
||
```
|
||
|
||
**正确代码**:
|
||
```javascript
|
||
// ✅ 正确: 使用包装器捕获所有错误
|
||
const handleSendCode = async () => {
|
||
try {
|
||
await sendCode();
|
||
} catch (error) {
|
||
// 错误已在 sendCode 内部处理
|
||
console.error('Send code error (caught in handler):', error);
|
||
}
|
||
};
|
||
|
||
<Button onClick={handleSendCode}>
|
||
发送验证码
|
||
</Button>
|
||
```
|
||
|
||
---
|
||
|
||
### 10.2 调试工具
|
||
|
||
#### Chrome DevTools Network 面板
|
||
|
||
**检查认证请求**:
|
||
```
|
||
1. 打开 DevTools → Network 面板
|
||
2. 过滤: 输入 "auth"
|
||
3. 观察请求:
|
||
- Request Headers: Cookie 是否存在
|
||
- Response Headers: Set-Cookie 是否正确
|
||
- Response Body: 返回数据是否正确
|
||
4. 检查状态码:
|
||
- 200: 成功
|
||
- 401: 未授权 (Session 过期)
|
||
- 400: 参数错误
|
||
- 500: 服务器错误
|
||
```
|
||
|
||
#### React DevTools
|
||
|
||
**检查状态**:
|
||
```
|
||
1. 安装 React DevTools 扩展
|
||
2. 打开 DevTools → Components 面板
|
||
3. 选择 AuthProvider 组件
|
||
4. 查看 props 和 hooks:
|
||
- user: 用户信息
|
||
- isAuthenticated: 是否登录
|
||
- isLoading: 加载状态
|
||
```
|
||
|
||
#### Console 日志
|
||
|
||
**关键调试点**:
|
||
```javascript
|
||
// AuthContext.js
|
||
console.log('🔍 检查Session状态...');
|
||
console.log('✅ Session有效:', data);
|
||
console.log('❌ Session检查错误:', error);
|
||
|
||
// WechatRegister.js
|
||
console.log('备用轮询:启动备用轮询机制');
|
||
console.log('备用轮询:检查微信状态');
|
||
|
||
// AuthFormContent.js
|
||
console.log('Auth error:', error);
|
||
```
|
||
|
||
---
|
||
|
||
### 10.3 后端日志
|
||
|
||
**Flask 日志配置**:
|
||
```python
|
||
import logging
|
||
|
||
logging.basicConfig(
|
||
level=logging.DEBUG,
|
||
format='%(asctime)s [%(levelname)s] %(message)s'
|
||
)
|
||
|
||
@app.route('/api/auth/login-with-code', methods=['POST'])
|
||
def login_with_verification_code():
|
||
logging.info(f"登录请求: {request.json}")
|
||
|
||
try:
|
||
# 验证码验证逻辑
|
||
logging.debug(f"验证码检查: {code}")
|
||
|
||
if verified:
|
||
logging.info(f"登录成功: user_id={user.id}")
|
||
else:
|
||
logging.warning(f"验证码错误: {phone}")
|
||
except Exception as e:
|
||
logging.error(f"登录失败: {str(e)}", exc_info=True)
|
||
```
|
||
|
||
---
|
||
|
||
## 附录
|
||
|
||
### A. 相关文件清单
|
||
|
||
**前端核心文件**:
|
||
```
|
||
src/
|
||
├── contexts/
|
||
│ ├── AuthContext.js # 全局认证状态
|
||
│ └── AuthModalContext.js # 弹窗状态管理
|
||
├── components/
|
||
│ ├── Auth/
|
||
│ │ ├── AuthFormContent.js # 认证表单
|
||
│ │ ├── AuthModalManager.js # 弹窗管理器
|
||
│ │ ├── WechatRegister.js # 微信扫码
|
||
│ │ ├── VerificationCodeInput.js # 验证码输入
|
||
│ │ └── AuthHeader.js # 表单头部
|
||
│ ├── ProtectedRoute.js # 路由保护
|
||
│ └── Navbars/
|
||
│ └── HomeNavbar.js # 导航栏
|
||
├── services/
|
||
│ └── authService.js # 认证API服务
|
||
├── views/
|
||
│ └── Pages/
|
||
│ └── WechatCallback.js # 微信H5回调页
|
||
├── layouts/
|
||
│ └── Home.js # 首页布局
|
||
└── App.js # 应用根组件
|
||
```
|
||
|
||
**后端核心文件**:
|
||
```
|
||
/
|
||
├── app.py # Flask主应用
|
||
│ ├── /api/auth/session # Session检查
|
||
│ ├── /api/auth/send-verification-code # 发送验证码
|
||
│ ├── /api/auth/login-with-code # 验证码登录
|
||
│ ├── /api/auth/wechat/qrcode # 微信二维码
|
||
│ ├── /api/auth/wechat/check # 检查扫码状态
|
||
│ ├── /api/auth/login/wechat # 微信登录
|
||
│ ├── /api/auth/wechat/h5-auth # 微信H5授权
|
||
│ ├── /api/auth/wechat/h5-callback # 微信H5回调
|
||
│ └── /api/auth/logout # 退出登录
|
||
└── wechat_pay_config.py # 微信配置
|
||
```
|
||
|
||
### B. 环境变量配置
|
||
|
||
**.env 文件**:
|
||
```bash
|
||
# 生产/开发环境标识
|
||
NODE_ENV=development
|
||
|
||
# API基础地址 (开发环境)
|
||
REACT_APP_API_URL=http://49.232.185.254:5001
|
||
|
||
# 微信开放平台配置
|
||
WECHAT_APP_ID=your_app_id
|
||
WECHAT_APP_SECRET=your_app_secret
|
||
WECHAT_REDIRECT_URI=https://yourdomain.com/api/auth/wechat/callback
|
||
|
||
# 短信服务配置
|
||
SMS_PROVIDER=tencent_cloud
|
||
SMS_APP_ID=your_sms_app_id
|
||
SMS_APP_KEY=your_sms_app_key
|
||
```
|
||
|
||
### C. 参考文档
|
||
|
||
- [React Context API](https://react.dev/reference/react/useContext)
|
||
- [Chakra UI](https://chakra-ui.com/)
|
||
- [Flask Session](https://flask.palletsprojects.com/en/2.3.x/api/#sessions)
|
||
- [微信开放平台](https://open.weixin.qq.com/)
|
||
- [微信网页授权](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html)
|
||
|
||
---
|
||
|
||
**文档版本**: v2.0
|
||
**最后更新**: 2025-01-16
|
||
**维护者**: 价值前沿技术团队
|