6.2 KiB
6.2 KiB
微信登录与教师身份实施方案
本文用于把当前客户端可伪造的固定 X-User-Id,替换为“微信身份换取后端会话”的正式登录体系。完成前不应公开发布小程序。
目标流程
小程序调用 wx.login()
-> 将一次性 code 发送到 Rust POST /api/v1/auth/wechat/login
-> Rust 使用服务端 AppID、AppSecret 调用微信 code2Session
-> 微信返回 openid(unionid 可选)
-> Rust 查找或创建内部教师 UUID
-> Rust 签发项目自己的随机会话令牌
-> 小程序以 Authorization: Bearer <token> 调用普通接口和上传录音
微信官方入口:
WECHAT_APP_SECRET 只能保存在 Rust 服务环境变量中。小程序不得包含 AppSecret,也不得接收或保存微信 session_key。当前阶段只需要 openid 识别当前小程序中的教师。
数据模型
新增三张表:
users
id uuid primary key
status text not null
created_at timestamptz not null
updated_at timestamptz not null
wechat_identities
app_id text not null
openid text not null
unionid text null
user_id uuid not null references users(id)
created_at timestamptz not null
last_login_at timestamptz not null
primary key (app_id, openid)
auth_sessions
id uuid primary key
user_id uuid not null references users(id)
token_hash bytea not null unique
expires_at timestamptz not null
last_used_at timestamptz not null
revoked_at timestamptz null
created_at timestamptz not null
业务表继续使用现有 owner_id UUID,不直接使用 openid。这样微信身份变化、未来增加手机号或管理员账号时,不需要重写业务数据。
后端接口
POST /api/v1/auth/wechat/login
请求:
{ "code": "wx.login 返回的一次性 code" }
处理要求:
- 校验 code 非空并限制长度和登录频率。
- Rust 使用
WECHAT_APP_ID、WECHAT_APP_SECRET调用code2Session。 - 微信返回错误码、缺少
openid或网络失败时返回明确的401/502,日志不得记录 AppSecret、session_key 或完整 code。 - 按
(app_id, openid)原子地查找或创建本地用户。 - 生成至少 32 字节的加密安全随机令牌,只把 SHA-256 哈希写入数据库。
- 原始令牌只在本次响应中返回,建议有效期 30 天。
响应:
{
"access_token": "后端随机会话令牌",
"expires_at": "2026-08-20T00:00:00Z",
"user_id": "内部教师 UUID"
}
同时增加:
GET /api/v1/auth/me:返回当前内部用户 ID 和会话到期时间。POST /api/v1/auth/logout:撤销当前令牌。
业务接口鉴权
实现统一的 Axum 身份 extractor:
- 读取
Authorization: Bearer <token>。 - 对 token 做 SHA-256 后查询
auth_sessions。 - 拒绝不存在、已撤销、已过期或用户已停用的会话。
- 将数据库中的
user_id作为业务owner_id,客户端不能提交或覆盖它。
普通 JSON 请求和 wx.uploadFile 必须使用相同的 Bearer token。不得把 openid、user_id 或固定 UUID 当成可信请求头。
本地开发可暂时保留 X-User-Id,但必须由显式环境变量控制:
ALLOW_DEVELOPMENT_USER_HEADER=false
生产默认和 compose.deploy.yml 必须为 false。只有本地测试环境可设置为 true,并在日志中显示开发鉴权已开启。
小程序登录状态
新增独立 utils/auth.ts:
- 从微信 Storage 读取后端 access token 和到期时间。
- 没有有效 token 时调用
wx.login(),再调用后端登录接口。 - 使用共享的
loginPromise合并并发登录,避免多个页面同时换取 code。 - 所有
wx.request和wx.uploadFile自动附加 Bearer token。 - 收到一次
401时清理本地 token、重新登录并重试原请求一次,禁止无限重试。 - 退出登录时先调用后端撤销接口,再清理 Storage。
“我的”页面移除固定开发 UUID,改为显示登录状态和内部教师 ID;不显示 openid、session_key 或完整 access token。
现有开发数据归属
当前数据属于固定开发用户:
5a4da7f0-d70c-465f-bcab-124c504aa9f0
不能把这些数据自动交给“部署后第一个登录的人”,否则存在抢占风险。采用一次性管理命令迁移:
- 先发布微信登录功能,由目标微信账号完成一次登录并取得新的内部用户 UUID。
- 在服务端运行受控 CLI,例如:
migrate-user-owner --from 5a4da7f0-d70c-465f-bcab-124c504aa9f0 --to <新用户UUID>
- CLI 在单个数据库事务中更新学生档案、预设、反馈记录和反馈会话的
owner_id。 - 遇到目标账号已有同类数据或唯一约束冲突时中止并报告,不做部分迁移。
- 迁移前备份数据库,迁移后比较每张表的记录数并进行页面验收。
CLI 不接收 openid 或 AppSecret;它只合并两个经过后端确认的内部 UUID,便于审计和回滚。
分阶段交付
- 数据库迁移、微信 API 客户端、登录接口和会话存储。
- 后端统一 Bearer extractor,同时在本地保留受控的开发头兼容模式。
- 小程序登录管理、普通请求和录音上传的自动鉴权与一次重试。
- 自动化测试:code2Session 模拟、令牌过期/撤销、跨用户隔离、上传鉴权和并发登录。
- 部署环境变量并验证目标微信账号登录。
- 运行一次性旧数据迁移,关闭生产
X-User-Id,最后再提交体验版测试。
发布验收
- 两个不同微信账号看到的数据互相隔离。
- 客户端伪造
X-User-Id、openid或user_id无法越权。 - access token 过期或撤销后必须重新登录。
- 普通接口和录音上传均能在 token 刷新后重试一次。
- AppSecret、session_key 和令牌未出现在代码、Git、日志或错误响应中。
- 原开发用户数据仅归属指定的目标微信账号。