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

48 KiB
Raw Permalink Blame History

价值前沿认证系统完整文档

版本: 2.0 更新日期: 2025-01-16 作者: Claude Code 适用范围: 前端 React + 后端 Flask


📖 目录

  1. 系统架构概览
  2. 认证流程详解
  3. 路由配置与跳转逻辑
  4. API接口文档
  5. 前端组件架构
  6. 状态管理机制
  7. Session持久化
  8. 错误处理策略
  9. 安全机制
  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 路由结构

// 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)

职责: 认证表单主体内容

功能:

  1. 手机号输入
  2. 验证码输入 + 发送验证码
  3. 登录/注册按钮
  4. 微信扫码 (桌面端右侧)
  5. 微信H5登录 (移动端验证码下方图标)
  6. 隐私协议链接

布局:

桌面端 (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 (可选) 前端规范

优势:

  • 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: 验证码提示"已过期或不存在"

可能原因:

  1. Session cookie 未正确发送
  2. 前后端跨域配置问题
  3. 验证码确实过期 (>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: 刷新页面后登录状态丢失

可能原因:

  1. checkSession() 未在 App 启动时调用
  2. Cookie 被浏览器阻止 (隐私模式/第三方 Cookie 限制)
  3. 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);
  }
};

解决方法:

  • 确认 AuthContextApp.js 根级别
  • 检查浏览器 Cookie 设置 (允许第三方 Cookie)
  • 检查 Session 是否过期 (后端日志)

问题3: 微信扫码后前端未收到状态更新

可能原因:

  1. 轮询已停止 (组件卸载)
  2. 后端回调未正确更新状态
  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 维护者: 价值前沿技术团队