- 移动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>
48 KiB
价值前沿认证系统完整文档
版本: 2.0 更新日期: 2025-01-16 作者: Claude Code 适用范围: 前端 React + 后端 Flask
📖 目录
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
认证方式:
- 手机验证码登录 (短信验证码)
- 微信PC扫码登录 (二维码扫码)
- 微信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-codeapp.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/qrcodeapp.py:2560- POST /api/auth/wechat/checkapp.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授权APIsrc/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 路由结构
// 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)
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
用途: 检查当前用户登录状态
请求:
GET /api/auth/session HTTP/1.1
Cookie: session=xxx
响应:
{
"isAuthenticated": true,
"user": {
"id": 123,
"phone": "13800138000",
"nickname": "价小前用户",
"email": "user@example.com",
"avatar_url": "https://...",
"has_wechat": true
}
}
前端调用:
// 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
用途: 发送手机验证码
请求:
POST /api/auth/send-verification-code HTTP/1.1
Content-Type: application/json
{
"credential": "13800138000",
"type": "phone",
"purpose": "login"
}
响应:
{
"success": true,
"message": "验证码已发送"
}
错误响应:
{
"success": false,
"error": "手机号格式不正确"
}
前端调用:
// 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
用途: 使用验证码登录/注册
请求:
POST /api/auth/login-with-code HTTP/1.1
Content-Type: application/json
{
"credential": "13800138000",
"verification_code": "123456",
"login_type": "phone"
}
响应 (登录成功):
{
"success": true,
"isNewUser": false,
"user": {
"id": 123,
"phone": "13800138000",
"nickname": "价小前用户"
}
}
响应 (注册成功):
{
"success": true,
"isNewUser": true,
"user": {
"id": 124,
"phone": "13900139000",
"nickname": "用户13900139000"
}
}
错误响应:
{
"success": false,
"error": "验证码已过期或不存在"
}
自动注册逻辑:
- 如果手机号不存在 → 自动创建新用户
- 默认昵称:
用户{手机号} - isNewUser 标记用于前端引导设置昵称
4.4 获取微信二维码
接口: GET /api/auth/wechat/qrcode
用途: 获取微信PC扫码登录二维码
请求:
GET /api/auth/wechat/qrcode HTTP/1.1
响应:
{
"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"
}
前端调用:
// src/services/authService.js:69
getWechatQRCode: async () => {
return await apiRequest('/api/auth/wechat/qrcode');
}
二维码有效期: 5分钟
4.5 检查微信扫码状态
接口: POST /api/auth/wechat/check
用途: 轮询检查微信扫码状态
请求:
POST /api/auth/wechat/check HTTP/1.1
Content-Type: application/json
{
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
响应状态:
| status | 说明 | 前端行为 |
|---|---|---|
| waiting | 等待扫码 | 继续轮询 |
| scanned | 已扫码,等待确认 | 显示提示 + 继续轮询 |
| authorized | 已授权 | 继续轮询 |
| login_success | 登录成功 (老用户) | 停止轮询 + 调用登录接口 |
| register_success | 注册成功 (新用户) | 停止轮询 + 调用登录接口 |
| expired | 二维码过期 | 停止轮询 + 显示刷新按钮 |
响应示例:
{
"status": "login_success",
"user_info": {
"openid": "xxx",
"nickname": "微信用户",
"avatar_url": "https://..."
}
}
前端轮询:
// src/components/Auth/WechatRegister.js:180
pollIntervalRef.current = setInterval(() => {
checkWechatStatus();
}, 2000); // 每2秒检查一次
4.6 微信登录
接口: POST /api/auth/login/wechat
用途: 使用微信 session_id 完成登录
请求:
POST /api/auth/login/wechat HTTP/1.1
Content-Type: application/json
{
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
响应:
{
"success": true,
"user": {
"id": 123,
"nickname": "微信用户",
"avatar_url": "https://...",
"has_wechat": true
},
"token": "optional-token"
}
前端调用:
// 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网页授权链接
请求:
POST /api/auth/wechat/h5-auth HTTP/1.1
Content-Type: application/json
{
"redirect_url": "https://yourdomain.com/home/wechat-callback"
}
响应:
{
"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"
}
前端调用:
// 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授权回调
请求:
POST /api/auth/wechat/h5-callback HTTP/1.1
Content-Type: application/json
{
"code": "wechat-code-xxx",
"state": "csrf-state-yyy"
}
响应:
{
"success": true,
"user": {
"id": 123,
"nickname": "微信用户",
"avatar_url": "https://..."
},
"token": "optional-token"
}
前端调用:
// 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
请求:
POST /api/auth/logout HTTP/1.1
Cookie: session=xxx
响应:
{
"success": true,
"message": "已退出登录"
}
前端调用:
// 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)
职责: 全局认证状态管理
状态:
{
user: {
id: 123,
phone: "13800138000",
nickname: "价小前用户",
email: "user@example.com",
avatar_url: "https://...",
has_wechat: true
},
isAuthenticated: true,
isLoading: false
}
方法:
checkSession()- 检查当前登录状态logout()- 退出登录
使用:
import { useAuth } from '../../contexts/AuthContext';
const { user, isAuthenticated, isLoading, checkSession, logout } = useAuth();
AuthModalContext (src/contexts/AuthModalContext.js)
职责: 认证弹窗状态管理
状态:
{
isAuthModalOpen: false,
redirectUrl: null, // 登录成功后跳转的URL (可选)
}
方法:
openAuthModal(url, callback)- 打开认证弹窗closeModal()- 关闭弹窗 (未登录则跳转首页)handleLoginSuccess(user)- 登录成功处理
使用:
import { useAuthModal } from '../../contexts/AuthModalContext';
const { isAuthModalOpen, openAuthModal, handleLoginSuccess } = useAuthModal();
// 打开弹窗
openAuthModal();
// 登录成功
handleLoginSuccess(userData);
AuthModalManager (src/components/Auth/AuthModalManager.js)
职责: 渲染认证弹窗 UI
特点:
- 响应式尺寸 (移动端 md, 桌面端 xl)
- 条件渲染 (仅在打开时渲染,避免不必要的Portal)
- 半透明背景 + 模糊效果
- 禁止点击背景关闭 (防止误操作)
响应式配置:
const modalSize = useBreakpointValue({
base: "md", // 移动端
md: "lg", // 中屏
lg: "xl" // 大屏
});
const modalMaxW = useBreakpointValue({
base: "90%", // 移动端占90%宽度
md: "700px" // 桌面端固定700px
});
AuthFormContent (src/components/Auth/AuthFormContent.js)
职责: 认证表单主体内容
功能:
- 手机号输入
- 验证码输入 + 发送验证码
- 登录/注册按钮
- 微信扫码 (桌面端右侧)
- 微信H5登录 (移动端验证码下方图标)
- 隐私协议链接
布局:
桌面端 (Desktop):
┌─────────────────────────────────────┐
│ 价值前沿 - 开启您的投资之旅 │
├──────────────────┬──────────────────┤
│ 登陆/注册 │ 微信扫码 │
│ │ │
│ 手机号输入框 │ [二维码区域] │
│ 验证码输入框 │ │
│ [获取验证码] │ 请使用微信扫码 │
│ │ │
│ [登录/注册按钮] │ │
│ │ │
│ 《隐私政策》 │ │
└──────────────────┴──────────────────┘
移动端 (Mobile):
┌─────────────────────┐
│ 价值前沿 │
│ 开启您的投资之旅 │
├─────────────────────┤
│ 登陆/注册 │
│ │
│ 手机号输入框 │
│ │
│ 验证码输入框 │
│ [获取验证码] │
│ 其他登录方式: [微信] │
│ │
│ [登录/注册按钮] │
│ │
│ 《隐私政策》 │
└─────────────────────┘
关键逻辑:
// 发送验证码 (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 (二维码过期 → 显示"点击刷新")
轮询机制:
// 主轮询: 每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);
二维码缩放:
// 使用 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:
{
value: string, // 验证码值
onChange: function, // 输入变化回调
onSendCode: function, // 发送验证码回调
countdown: number, // 倒计时秒数
isLoading: boolean, // 表单加载状态
isSending: boolean, // 发送中状态
error: string, // 错误信息
placeholder: string, // 输入框提示
buttonText: string, // 按钮文本
countdownText: function, // 倒计时文本生成器
colorScheme: string, // 按钮颜色
isRequired: boolean // 是否必填
}
错误处理包装器:
// 确保所有错误都被捕获,防止被 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)
职责: 路由保护组件
保护策略:
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):
<Button
colorScheme="blue"
variant="solid"
size="sm"
onClick={() => openAuthModal()}
>
登录 / 注册
</Button>
已登录状态 (src/components/Navbars/HomeNavbar.js:523):
<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 → 返回用户信息
↓
前端恢复登录状态 ✅
跨标签页同步 (当前未实现):
// 建议实现方式
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):
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):
fetch('/api/auth/session', {
credentials: 'include' // ✅ 必须包含,否则 cookie 不会发送
});
7.2 跨域处理
开发环境 (craco.config.js):
proxy: {
'/api': {
target: 'http://49.232.185.254:5001',
changeOrigin: true,
secure: false,
logLevel: 'debug'
}
}
生产环境:
- 前后端同域: 无需额外配置
- 前后端跨域:
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 超时处理:
// 在关键 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 错误分类
根据 错误处理文档,错误分为4类:
| 类别 | 场景 | 处理方式 |
|---|---|---|
| A类 | 致命错误 (组件崩溃) | ErrorBoundary 捕获 + 显示错误页面 |
| B类 | 用户操作失败 | Toast 提示 + 不中断页面 |
| C类 | 数据加载失败 | 优雅降级 + 显示空状态 |
| D类 | 后台任务失败 | 静默处理 + 控制台日志 |
8.2 认证相关错误处理
发送验证码失败 (B类):
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类):
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类):
// 备用轮询机制: 静默处理错误
backupPollIntervalRef.current = setInterval(() => {
try {
checkWechatStatus().catch(error => {
// ✅ 静默处理,只记录日志
console.warn('备用轮询检查失败(静默处理):', error);
});
} catch (error) {
console.warn('备用轮询执行出错(静默处理):', error);
}
}, 3000);
按钮点击错误防护:
// 防止未捕获的 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 窃取)
- ✅ 自动随请求发送 (无需手动管理)
- ✅ 服务器端管理 (更安全)
配置:
# 后端设置
@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 验证码安全
限流规则:
# 同一手机号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)
}
验证逻辑:
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: 验证码提示"已过期或不存在"
可能原因:
- Session cookie 未正确发送
- 前后端跨域配置问题
- 验证码确实过期 (>5分钟)
排查步骤:
# 1. 检查 fetch 请求是否包含 credentials
console.log('Fetch options:', {
credentials: 'include' // ✅ 必须包含
});
# 2. 检查浏览器 Network 面板
- Request Headers 是否包含 Cookie
- Response Headers 是否包含 Set-Cookie
# 3. 检查后端日志
- Session 是否正确存储验证码
- 验证码是否已过期
解决方法:
// ✅ 确保所有认证相关请求都包含 credentials
fetch('/api/auth/send-verification-code', {
method: 'POST',
credentials: 'include', // 关键!
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({...})
});
问题2: 刷新页面后登录状态丢失
可能原因:
checkSession()未在 App 启动时调用- Cookie 被浏览器阻止 (隐私模式/第三方 Cookie 限制)
- Session 已过期
排查步骤:
// 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: 微信扫码后前端未收到状态更新
可能原因:
- 轮询已停止 (组件卸载)
- 后端回调未正确更新状态
session_id不匹配
排查步骤:
// 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
错误代码:
// ❌ 错误: 直接传递 async 函数
<Button onClick={async () => {
await sendCode(); // 如果失败,会被 ErrorBoundary 捕获
}}>
发送验证码
</Button>
正确代码:
// ✅ 正确: 使用包装器捕获所有错误
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 日志
关键调试点:
// 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 日志配置:
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 文件:
# 生产/开发环境标识
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. 参考文档
文档版本: v2.0 最后更新: 2025-01-16 维护者: 价值前沿技术团队