Files
vf_react/AUTHENTICATION_SYSTEM_GUIDE.md
zdl bc407d2a35 docs: 添加认证系统完整指南文档
- 详细的认证系统架构说明
- 三种认证方式的实现细节(手机验证码、微信PC、微信H5)
- API 接口文档
- 组件架构说明
- 调试和故障排查指南

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:52:56 +08:00

1880 lines
48 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.

# 价值前沿认证系统完整文档
> **版本**: 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
**维护者**: 价值前沿技术团队