登录与注册完整流程分析

登录与注册完整流程分析

一、整体架构概览

复制代码
┌──────────────────────────────────────────────────────────────┐
│  前端 (SPA - static/index.html)                              │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  ┌───────────┐  │
│  │HTML 表单  │→│ 事件绑定  │→│ ApiService │→│ UIManager  │  │
│  │(451-561) │  │(1339-69) │  │ (805-1079)│  │(1466-1612)│  │
│  └──────────┘  └──────────┘  └─────┬─────┘  └───────────┘  │
│                                    │ HTTP (fetch + JSON)     │
└────────────────────────────────────┼──────────────────────────┘
                                     │
┌────────────────────────────────────┼──────────────────────────┐
│  后端 (Flask - app.py)            │                          │
│  ┌───────────────┐  ┌─────────────┴───────────┐              │
│  │ get_user_from │  │ /api/auth/register (270) │              │
│  │ _session (159)│  │ /api/auth/login    (333) │              │
│  │ require_auth  │  │ /api/auth/logout   (376) │              │
│  │ (184)         │  │ /api/users/me      (400) │              │
│  └───────────────┘  └─────────────┬───────────┘              │
│                                   │                           │
└───────────────────────────────────┼───────────────────────────┘
                                    │
┌───────────────────────────────────┼───────────────────────────┐
│  数据层 (database/db_manager.py)  │                           │
│  ┌──────────────┐  ┌──────────────┴──────────┐               │
│  │ models.py    │  │ DBManager                │               │
│  │ User (15)    │  │ create_user (25)         │               │
│  │ UserSession  │  │ verify_password (79)     │               │
│  │ (180)        │  │ create_session (357)     │               │
│  │ ExperimentLog│  │ get_user_by_session_     │               │
│  │ (152)        │  │   token (376)            │               │
│  └──────────────┘  └─────────────────────────┘               │
└──────────────────────────────────────────────────────────────┘

二、注册流程(从点击到数据库返回)

步骤1:用户触发注册入口

用户在页面上点击 "登录/注册" 按钮:

复制代码
// [index.html:1339]
this.elements.loginBtn.addEventListener('click', () => this.showAuthModal());

showAuthModal() 弹出模态框,默认显示登录表单 。用户点击 "没有账户?立即注册"

复制代码
// [index.html:1351]
this.elements.switchToRegister.addEventListener('click', () => this.showRegisterForm());

步骤2:伦理知情同意拦截

showRegisterForm() 不会直接显示注册表单,而是先弹出伦理同意模态框:

复制代码
// [index.html:1485-1492]
showRegisterForm(force = false) {
    if (!force && !this.ethicsConsentAccepted) {
        this.showEthicsConsentModal();  // 弹出伦理同意弹窗 (z-index: 60, 比登录框更高)
        return;  // ← 拦截,不显示注册表单
    }
    // 只有 force=true 或已同意时才显示注册表单
    if (this.elements.loginForm) this.elements.loginForm.classList.add('hidden');
    if (this.elements.registerForm) this.elements.registerForm.classList.remove('hidden');
}

伦理同意模态框 HTML 结构 index.html:564-615

  • 显示研究课题信息:华东师范大学心理与认知科学学院的记忆系统研究
  • 包含一个指向 /ecnu_ethics_notice.html 的链接
  • 包含一个 checkbox:id="ethics-consent-checkbox"
  • "同意并继续注册" 按钮 → confirmEthicsConsent() index.html:1369

用户勾选 checkbox 并点击"同意并继续注册":

复制代码
// [index.html:1516-1525]
confirmEthicsConsent() {
    if (!this.elements.ethicsConsentCheckbox?.checked) {
        this.showNotification('请先勾选知情同意后再继续注册');
        return;
    }
    this.ethicsConsentAccepted = true;   // ← 设置标记
    this.hideEthicsConsentModal();        // 关闭伦理弹窗
    this.showRegisterForm(true);          // force=true,跳过伦理检查
}

步骤3:填写并提交注册表单

注册表单字段 index.html:493-559

字段 DOM ID 类型
用户名 register-username text
密码 register-password password
姓名 register-name text
年龄 register-age number (1-100)
性别 register-gender select (男/女/其他)
记忆组别 register-memory-group select (A/B/C/D/E)

记忆组别映射:

复制代码
A → sensory_memory        (感觉记忆)
B → working_memory        (工作记忆)
C → gist_memory           (要旨记忆)
D → perfect_recall_memory (完美回忆)
E → hybrid_memory         (混合记忆)

点击 "注册" 按钮触发 index.html:1360

复制代码
this.elements.submitRegister.addEventListener('click', () => this.register());

步骤4:前端校验与请求发送

复制代码
// [index.html:1555-1600]
async register() {
    // 1. 从DOM读取所有字段值
    const username = this.elements.registerUsername?.value.trim();
    const password = this.elements.registerPassword?.value;
    const name = this.elements.registerName?.value.trim();
    const age = this.elements.registerAge?.value;
    const gender = this.elements.registerGender?.value;
    const memoryGroup = this.elements.registerMemoryGroup?.value;

    // 2. 前端非空校验
    if (!username || !password || !name || !age || !gender) {
        this.showNotification('请填写所有必填字段');
        return;
    }

    // 3. 再次检查伦理同意(防止绕过)
    if (!this.ethicsConsentAccepted) {
        this.showEthicsConsentModal();
        this.showNotification('请先完成知情同意');
        return;
    }

    // 4. 调用 ApiService 发送 POST 请求
    const response = await ApiService.register({
        username,
        password,          // ← 明文密码!通过 HTTPS 传输
        name,
        age: parseInt(age),
        gender,
        memory_group: memoryGroup,
        ethics_consent_accepted: true
    });
    // ...
}

步骤5:HTTP 请求到达后端

ApiService.register() index.html:874-879 本质上就是一个 POST 请求:

复制代码
async register(userData) {
    return this.request('/auth/register', {
        method: 'POST',
        body: JSON.stringify(userData)
    });
}

通用的 request() 方法 index.html:810-850 做了三件事:

  1. 拼接 URL:/api + /auth/register = /api/auth/register
  2. 设置 Content-Type: application/json
  3. 如果有 session token,自动附加 Authorization: Bearer <token>(注册时没有)

步骤6:后端路由 → 参数校验

复制代码
# [app.py:270-330]
@app.route('/api/auth/register', methods=['POST'])
def register():
    data = request.get_json() or {}

    username = data.get('username')
    password = data.get('password')
    name = data.get('name')
    age = data.get('age')
    gender = data.get('gender')
    memory_group = data.get('memory_group', 'sensory_memory')
    ethics_consent_accepted = data.get('ethics_consent_accepted', False)

    # 校验1: 必填字段
    if not all([username, password, name, age, gender]):
        return api_response(False, message='请填写所有必填字段')

    # 校验2: 伦理同意(后端二次校验,防止直接调API绕过前端)
    if ethics_consent_accepted is not True:
        return api_response(False, message='注册前请先同意伦理说明')

    # 校验3: 记忆组别合法性
    if memory_group not in MEMORY_GROUPS:
        return api_response(False, message='无效的记忆组别')

步骤7:数据库操作 --- 创建用户

复制代码
# [app.py:292-303]
db, session = get_db()   # 获取 DBManager 实例和 SQLAlchemy Session
try:
    user = db.create_user(
        user_id=username,    # ← 注意:user_id 就是 username!
        username=username,
        name=name,
        password=password,   # 明文传入,在 create_user 内部哈希
        age=age,
        gender=gender,
        memory_group=memory_group,
        user_type='normal'
    )

    if not user:
        return api_response(False, message='用户名已存在')

深入到 DBManager.create_user() db_manager.py:25-66

复制代码
def create_user(self, user_id, username, name, password, age=None,
                gender=None, memory_group='sensory_memory', user_type='normal'):
    # 1. 查重 ------ 检查 user_id(即用户名)是否已存在
    if self.get_user(user_id):   # SELECT * FROM users WHERE user_id = ?
        return None

    # 2. 构造 User SQLAlchemy 对象
    user = User(
        user_id=user_id,
        username=username,
        name=name,
        age=age,
        gender=gender,
        memory_group=memory_group,
        user_type=user_type,
        password_hash=self._hash_password(password),  # ← 密码在此处被哈希
        settings={'responseStyle': 'high', 'aiAvatar': 'human'},  # 默认设置
        demographics={},
        experiment_phase=1       # 初始实验阶段为 1
    )

    # 3. 写入数据库
    try:
        self.session.add(user)     # INSERT INTO users (...)
        self.session.commit()      # COMMIT
        return user                # 返回 ORM 对象
    except IntegrityError:         # 并发冲突保护
        self.session.rollback()
        return None

密码哈希细节 db_manager.py:422-425

复制代码
@staticmethod
def _hash_password(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()

⚠️ 安全注意 :使用的是裸 SHA-256 ,没有盐值(salt),没有迭代。password_hash 列定义为 String(64),正好匹配 SHA-256 的 64 位十六进制输出。这在生产环境中是不够安全的。

步骤8:数据库操作 --- 创建会话(Session Token)

复制代码
# [app.py:308-309]
# 注册成功后立即创建会话(相当于自动登录)
token = db.create_session(username, ttl_hours=Config.SESSION_TTL_HOURS)

Config.SESSION_TTL_HOURS 来自 config.py:79,默认值 168 小时(7天) ,可通过环境变量 SESSION_TTL_HOURS 覆盖。

深入到 DBManager.create_session() db_manager.py:357-374

复制代码
def create_session(self, user_id: str, ttl_hours: int = 168) -> str:
    # 1. 先清理过期会话
    self.delete_expired_sessions()

    # 2. 生成 64 字符的随机十六进制 token(secrets.token_hex(32) → 64 chars)
    token = self.generate_session_token()   # = secrets.token_hex(32)

    # 3. 计算过期时间
    now = datetime.utcnow()
    expires_at = now + timedelta(hours=max(1, int(ttl_hours)))

    # 4. 创建 UserSession ORM 对象
    user_session = UserSession(
        session_token=token,
        user_id=user_id,
        created_at=now,
        expires_at=expires_at,
        last_accessed_at=now
    )
    self.session.add(user_session)   # INSERT INTO user_sessions (...)
    self.session.commit()
    return token                     # 返回 64 字符 token

对应的数据表模型 models.py:180-199

复制代码
class UserSession(Base):
    __tablename__ = 'user_sessions'

    id = Column(Integer, primary_key=True, autoincrement=True)
    session_token = Column(String(128), unique=True, nullable=False, index=True)
    user_id = Column(String(50), ForeignKey('users.user_id', ondelete='CASCADE'), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    expires_at = Column(DateTime, nullable=False, index=True)
    last_accessed_at = Column(DateTime, default=datetime.utcnow, nullable=False)

    user = relationship('User', back_populates='sessions')

关键设计 :这个项目的认证不使用 JWT ,也不使用 Flask Session。而是自己实现了一套 Bearer Token + 数据库存储 的方案。Token 是 secrets.token_hex(32) 生成的 64 字符随机字符串,存储在 user_sessions 表中,服务端通过查表验证。

步骤9:记录事件日志

复制代码
# [app.py:312]
db.log_event(username, 'register')

experiment_logs 表写入一条 event_type='register' 的记录。

步骤10:构建响应返回前端

复制代码
# [app.py:314-328]
return api_response(True, data={
    'session_token': token,        # 64 字符 token
    'user': {
        'id': user.user_id,
        'username': user.username,
        'name': user.name,
        'age': user.age,
        'gender': user.gender,
        'memory_group': user.memory_group,
        'user_type': user.user_type,
        'settings': user.settings,
        'created_at': user.created_at.isoformat() if user.created_at else None,
        'experiment_phase': user.experiment_phase
    }
})

响应 JSON 格式:

复制代码
{
  "success": true,
  "data": {
    "session_token": "a1b2c3...64位十六进制字符串",
    "user": {
      "id": "zhangsan",
      "username": "zhangsan",
      "name": "张三",
      "age": 25,
      "gender": "male",
      "memory_group": "working_memory",
      "user_type": "normal",
      "settings": {"responseStyle": "high", "aiAvatar": "human"},
      "created_at": "2026-06-02T10:30:00",
      "experiment_phase": 1
    }
  }
}

步骤11:前端处理注册成功

复制代码
// [index.html:1585-1593]
if (response.success) {
    // 1. token 和用户数据存入 localStorage(持久化)
    ApiService.setSession(response.data.session_token, response.data.user);

    // 2. 更新内存中的状态
    this.appState.currentUser = response.data.user;
    this.appState.userData = response.data.user;

    // 3. 关闭模态框
    this.hideAuthModal();

    // 4. 切换 UI:访客界面 → 用户界面
    await this.updateUIAfterLogin();

    // 5. 通知
    this.showNotification('注册成功');
}

ApiService.setSession() index.html:852-859

复制代码
setSession(token, userData = null) {
    this.sessionToken = token;                           // 内存
    localStorage.setItem('session_token', token);        // 持久化到浏览器
    if (userData) {
        localStorage.setItem('user_data', JSON.stringify(userData));
    }
}

三、登录流程

步骤1-2:与注册相同(打开模态框 → 停留在登录表单)

登录表单只有两个字段 index.html:463-478

  • login-username(text)
  • login-password(password)

步骤3:提交登录

点击"登录"按钮 index.html:1357UIManager.login() index.html:1527-1553

复制代码
async login() {
    const username = this.elements.loginUsername?.value.trim();
    const password = this.elements.loginPassword?.value;

    if (!username || !password) {
        this.showNotification('请填写用户名和密码');
        return;
    }

    const response = await ApiService.login({ username, password });
    // ... 后续处理同注册
}

HTTP 请求:POST /api/auth/login,Body: {"username": "zhangsan", "password": "123456"}

步骤4:后端密码验证

复制代码
# [app.py:333-373]
@app.route('/api/auth/login', methods=['POST'])
def login():
    data = request.get_json() or {}
    username = data.get('username')
    password = data.get('password')

    if not all([username, password]):
        return api_response(False, message='请填写用户名和密码')

    db, session = get_db()
    try:
        # 核心:密码验证
        if not db.verify_password(username, password):
            return api_response(False, message='用户名或密码错误')
        # ...

verify_password() db_manager.py:79-84

复制代码
def verify_password(self, user_id: str, password: str) -> bool:
    user = self.get_user(user_id)          # SELECT * FROM users WHERE user_id = ?
    if not user:
        return False                        # 用户不存在 → 返回 False
    return user.password_hash == self._hash_password(password)
    #      ↑ 比较数据库中存储的哈希 == 输入密码的哈希

⚠️ 安全细节 :用户不存在和密码错误返回相同的错误消息("用户名或密码错误"),这是正确的做法,防止用户名枚举攻击。但密码哈希用的是无盐 SHA-256。

步骤5-7:创建会话 → 记录日志 → 返回响应

与注册流程的步骤 8-11 完全相同。


四、自动登录(页面刷新恢复会话)

页面加载时 AppState.initialize() index.html:1105-1155

复制代码
async initialize() {
    ApiService.isInitializing = true;   // 抑制 401 的 UI 提示

    if (ApiService.sessionToken) {     // 检查 localStorage 中是否有 token
        console.log('检测到已保存的会话Token,尝试自动登录...');
        try {
            const userResponse = await ApiService.getCurrentUser();
            // → GET /api/users/me  (带 Authorization: Bearer <token>)
            if (userResponse.success) {
                this.currentUser = userResponse.data;
                ApiService.setSession(ApiService.sessionToken, userResponse.data);
                // 根据用户类型加载对应界面...
            } else {
                ApiService.clearSession();  // token 无效,清除
            }
        } catch (error) {
            ApiService.clearSession();
        }
    }
    ApiService.isInitializing = false;
}

GET /api/users/me 的处理链:

复制代码
请求 → require_auth 装饰器 [app.py:184-193]
     → get_user_from_session(request) [app.py:159-171]
         → 从 Authorization header 提取 Bearer token
         → db.get_user_by_session_token(token) [db_manager.py:376-396]
             → SELECT * FROM user_sessions WHERE session_token = ?
             → 检查 expires_at > now ?(过期则删除并返回 None)
             → SELECT * FROM users WHERE user_id = session.user_id
             → 返回 User ORM 对象
     → 如果 user 为 None → 返回 401 → 前端清除 token
     → 如果 user 存在 → 返回用户信息

五、登出流程

复制代码
// [index.html:1602-1612]
async logout() {
    await ApiService.logout();   // POST /api/auth/logout
    this.appState.currentUser = null;
    this.updateUIAfterLogout();
    this.showNotification('已退出登录');
}

后端 app.py:376-395

复制代码
@app.route('/api/auth/logout', methods=['POST'])
def logout():
    data = request.get_json(silent=True) or {}
    token = data.get('session_token')

    if not token:
        # 也可以从 Authorization header 中取
        auth_header = request.headers.get('Authorization')
        if auth_header and auth_header.startswith('Bearer '):
            token = auth_header[7:]

    # DELETE FROM user_sessions WHERE session_token = ?
    db.delete_session(token)
    return api_response(True)

前端同时清除 index.html:862-865

复制代码
clearSession() {
    this.sessionToken = null;
    localStorage.removeItem('session_token');
    localStorage.removeItem('user_data');
}

六、完整的时序图

复制代码
用户                  浏览器(index.html)           Flask(app.py)           DBManager            PostgreSQL
 │                        │                          │                      │                     │
 │  点击"登录/注册"        │                          │                      │                     │
 │───────────────────────→│                          │                      │                     │
 │                        │ showAuthModal()          │                      │                     │
 │                        │ showLoginForm()          │                      │                     │
 │                        │                          │                      │                     │
 │  点击"立即注册"          │                          │                      │                     │
 │───────────────────────→│                          │                      │                     │
 │                        │ showRegisterForm()       │                      │                     │
 │                        │ → ethicsConsentAccepted? │                      │                     │
 │                        │ → false → 弹出伦理弹窗    │                      │                     │
 │                        │                          │                      │                     │
 │  勾选+点击"同意并继续"    │                          │                      │                     │
 │───────────────────────→│                          │                      │                     │
 │                        │ confirmEthicsConsent()   │                      │                     │
 │                        │ → showRegisterForm(true) │                      │                     │
 │                        │                          │                      │                     │
 │  填写表单+点击"注册"     │                          │                      │                     │
 │───────────────────────→│                          │                      │                     │
 │                        │ register()               │                      │                     │
 │                        │ → 前端校验字段            │                      │                     │
 │                        │ → ApiService.register()  │                      │                     │
 │                        │                          │                      │                     │
 │                        │  POST /api/auth/register │                      │                     │
 │                        │  {username,password,...} │                      │                     │
 │                        │─────────────────────────→│                      │                     │
 │                        │                          │ 校验: 必填字段        │                     │
 │                        │                          │ 校验: 伦理同意        │                     │
 │                        │                          │ 校验: 记忆组别        │                     │
 │                        │                          │                      │                     │
 │                        │                          │ db.create_user()     │                     │
 │                        │                          │─────────────────────→│                     │
 │                        │                          │                      │ get_user(user_id)  │
 │                        │                          │                      │───────────────────→│
 │                        │                          │                      │←─── None (不存在)───│
 │                        │                          │                      │                     │
 │                        │                          │                      │ _hash_password()   │
 │                        │                          │                      │ sha256(pwd).hex()  │
 │                        │                          │                      │                     │
 │                        │                          │                      │ User(...)           │
 │                        │                          │                      │ session.add(user)  │
 │                        │                          │                      │ session.commit()   │
 │                        │                          │                      │───────────────────→│
 │                        │                          │                      │←─── User对象 ──────│
 │                        │                          │←─── User对象 ────────│                     │
 │                        │                          │                      │                     │
 │                        │                          │ db.create_session()  │                     │
 │                        │                          │─────────────────────→│                     │
 │                        │                          │                      │ delete_expired()    │
 │                        │                          │                      │ token_hex(32)      │
 │                        │                          │                      │ UserSession(...)   │
 │                        │                          │                      │ session.add(sess)  │
 │                        │                          │                      │ session.commit()   │
 │                        │                          │                      │───────────────────→│
 │                        │                          │←─── "a1b2c3..." ────│                     │
 │                        │                          │                      │                     │
 │                        │                          │ db.log_event('reg')  │                     │
 │                        │                          │─────────────────────→│                     │
 │                        │                          │                      │ ExperimentLog(...) │
 │                        │                          │                      │───────────────────→│
 │                        │                          │←─────────────────────│                     │
 │                        │                          │                      │                     │
 │                        │  {success:true,          │                      │                     │
 │                        │   session_token, user}   │                      │                     │
 │                        │←─────────────────────────│                      │                     │
 │                        │                          │                      │                     │
 │                        │ setSession(token, user)  │                      │                     │
 │                        │ → localStorage.setItem() │                      │                     │
 │                        │ updateUIAfterLogin()     │                      │                     │
 │                        │ → 隐藏访客内容            │                      │                     │
 │                        │ → 显示用户内容            │                      │                     │
 │                        │                          │                      │                     │
 │  看到"注册成功"         │                          │                      │                     │
 │←───────────────────────│                          │                      │                     │

七、关键设计要点总结

层面 设计决策 代码位置
认证方式 自建 Bearer Token(非 JWT,非 Session Cookie) app.py:161
Token 生成 secrets.token_hex(32) → 64字符随机字符串 db_manager.py:353
Token 存储 数据库 user_sessions 表,有过期时间 models.py:180
Token 有效期 168 小时(7天),通过 SESSION_TTL_HOURS 配置 config.py:79
密码哈希 SHA-256(无盐)⚠️ db_manager.py:423
客户端存储 localStorage(session_token + user_data) index.html:853
user_id 设计 user_id 就是 username(登录名即主键) db_manager.py:295
注册=注册+登录 注册成功后自动创建 session,无需二次登录 app.py:308
伦理拦截 双端校验:前端 ethicsConsentAccepted 标记 + 后端 ethics_consent_accepted 字段 index.html:1486, app.py:286
401 处理 前端拦截 401 → 自动清除 token → 触发 session-expired 事件 index.html:829-839
并发安全 create_userIntegrityError 回滚保护 db_manager.py:64
相关推荐
仙俊红1 小时前
线程池面试
python·面试·职场和发展
SilentSamsara2 小时前
爬虫工程化:Playwright + 反反爬 + 数据清洗管道实战
开发语言·爬虫·python·青少年编程·playwright
AI玫瑰助手2 小时前
Python函数:函数的返回值(return)与多值返回
开发语言·python·信息可视化
花果山~~程序猿2 小时前
快速认识python项目的虚拟环境
开发语言·python
gCode Teacher 格码致知2 小时前
Python教学:字符编码的四种环境-由Deepseek产生
开发语言·python
小江的记录本2 小时前
【JVM虚拟机】类加载机制:类加载器、双亲委派模型、好处、破坏双亲委派的场景(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
小陶来咯2 小时前
FunctionCall实现与Prompt调优
python·ai·prompt
AI 编程助手GPT2 小时前
ChatGPT 新手入门与实战操作指南
开发语言·人工智能·git·python·chatgpt
原创小甜甜3 小时前
OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space
java·开发语言·python