Files
teaching-feedback-assistant/WECHAT_AUTH_PLAN.md

176 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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