登录与注册完整流程分析
一、整体架构概览
┌──────────────────────────────────────────────────────────────┐
│ 前端 (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 做了三件事:
- 拼接 URL:
/api+/auth/register=/api/auth/register - 设置
Content-Type: application/json - 如果有 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:1357 → UIManager.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_user 有 IntegrityError 回滚保护 |
db_manager.py:64 |