425 lines
48 KiB
Markdown
425 lines
48 KiB
Markdown
# Doodle Store v3.5 用户鉴权与登录设计
|
||
|
||
> 更新日期:2026-10-15。
|
||
>
|
||
> 状态:方案及分阶段实施设计。服务端微信登录、真实账户查询、GET /me、JWT reason、限流已接入;小程序 HTTP 接入、资料生命周期及 Web 会话待实现。当前落点见 [服务端登录接入说明](./服务端登录接入说明.md)。
|
||
>
|
||
> 依据:[数据库设计](./数据库设计文档.md)、[云服务端迁移方案](../云服务端迁移方案.md)、[Server 基础设置](./Server基础设置.md)及下述实际代码。第一阶段优先完成小程序;Web 在此基础上演进。
|
||
|
||
## 1. 设计结论与阶段边界
|
||
|
||
首期采用 **微信身份验证 → Doodle 用户 → 24 小时 JWT → 每次请求核对当前账户**。小程序继续静默登录,浏览和本地生成不等待登录;收藏、历史、资料、上传等远端用户操作需要有效凭证。
|
||
|
||
| 问题 | 本次建议 |
|
||
| ------------------ | ------------------------------------------------------------------------------------------ |
|
||
| 用户是谁 | `users.id` 是唯一业务账户标识;迁入用户保留原 `_id`,收藏及下载继续关联它 |
|
||
| 如何证明微信身份 | 小程序 `wx.login` 取得一次性 code,服务端用固定 AppID/AppSecret 调用 `code2Session` |
|
||
| 后续 API 如何认人 | `Authorization: Bearer <Doodle JWT>`;不信任缓存的 user、OpenID、客户端 UUID 或自报角色 |
|
||
| 登录有效期 | 沿用现有默认 24 小时;到期重新执行微信静默登录,不增加 refresh token |
|
||
| 如何撤销权限 | 每次查询当前 `status`、`role`、`authVersion`;敏感写事务中再次检查 |
|
||
| 是否增加数据库表 | 小程序阶段不增加,现有 13 张表足够;会话表和登录身份表按后续触发条件引入 |
|
||
| 昵称、头像、手机号 | 昵称和头像由用户主动填写;登录不要求填写,不要求绑定手机号 |
|
||
| Web 如何共用账户 | 优先考虑通过现有小程序扫码确认登录,直接进入同一 `users.id`;网站微信 OAuth 是独立接入选项 |
|
||
| Web 如何保存登录态 | 后续使用服务端会话及 HttpOnly Cookie,与小程序共用账户检查和业务权限规则 |
|
||
|
||
这里的 24 小时是 Doodle 凭证期限,与微信 `session_key` 的期限、本地资料缓存期限各自独立。`authVersion` 撤销的是旧凭证,不会禁止持有人再次通过微信证明身份;需要禁止登录时必须禁用账户。
|
||
|
||
暂不建设密码注册、短信验证码、复杂 RBAC、设备管理、第三方登录平台或统一 OAuth 服务。未来 Web 框架尚未选择,本方案不依赖 Next.js 或特定部署商。
|
||
|
||
## 2. 现状与需要补齐的部分
|
||
|
||
### 2.1 doodle-mini 实际实现
|
||
|
||
| 代码 | 已有行为 | 迁移时的处理 |
|
||
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||
| [app.ts](../../../doodle-mini/miniprogram/app.ts) | `onLaunch` 非阻塞调用 `getUser()`;仍初始化 CloudBase | 保留非阻塞启动,替换身份及业务请求;CloudBase 随兼容阶段退场 |
|
||
| [utils/auth.ts](../../../doodle-mini/miniprogram/utils/auth.ts) | 内存 → 7 天 Storage → `userLogin`;已有 `loginPromise` 合并部分并发 | 用户展示缓存与凭证分开;首次登录、续期、401 共用同一个登录 Promise |
|
||
| [userLogin](../../../doodle-mini/cloudfunctions/userLogin/index.js) | 从 `getWXContext()` 获取身份;按 OpenID 查用户;新增或更新活跃时间;支持更新资料 | 改成服务端兑换 code;唯一约束保证并发;登录与资料更新拆成明确接口 |
|
||
| [utils/http.ts](../../../doodle-mini/miniprogram/utils/http.ts) | 基础 `wx.request` 包装 | 补齐 API 地址、PATCH、Bearer、结构化错误、401 恢复;上传和下载另做适配 |
|
||
| [个人中心](../../../doodle-mini/miniprogram/pages/profile/profile.ts) | 已实现主动选择头像、填写昵称、上传 CloudBase,再更新资料 | 保留交互;头像改为上传七牛资产后绑定 assetId |
|
||
| [收藏](../../../doodle-mini/miniprogram/utils/favorites.ts)、[下载](../../../doodle-mini/miniprogram/utils/downloadLogs.ts) | 调用前 `getUser()`;仍调用云函数;部分失败返回空列表或 0 | 改为受保护 API;错误不能假装成真实空数据 |
|
||
| [设置页面](../../../doodle-mini/miniprogram/supportPages/settings/settings.ts)、[userSettings](../../../doodle-mini/cloudfunctions/userSettings/index.js) | 清资料或注销后 `clearUser()`;旧注销只删 users | 清资料不清凭证;注销完整事务清理,阻止后台自动重建账户 |
|
||
|
||
需要纠正的几个历史描述:
|
||
|
||
- 当前 user 对象包含 OpenID/UnionID,云函数还通过展开数据库记录返回资料;新 DTO 必须显式列字段,不直接返回数据库模型。
|
||
- 7 天缓存只控制重新调用登录的时机,内存命中还会跳过 Storage 过期检查;它不提供独立 API 的身份验证。
|
||
- 旧登录先查再新增,没有原子冲突处理,不能把它描述为天然并发安全的 upsert。
|
||
- 头像昵称已在个人中心实现;旧登录文档中的“后续实现”已过时。
|
||
- `envVersion === 'develop'` 只是入口显示条件。[homeTimerControl](../../../doodle-mini/cloudfunctions/homeTimerControl/index.js) 等入口代码未见管理员身份检查;线上调用权限仍需盘点,迁移后的每个管理 API 都必须自行校验。
|
||
|
||
### 2.2 doodle-store 已有基础
|
||
|
||
[src/auth](../../apps/server/src/auth/auth.module.ts) 已有 JWT 签发/验签、全局 `AccessTokenGuard`、`@Public()`、`@Roles()`、`@CurrentUser()`。JWT 固定 HS256,检查 issuer、audience、过期时间及 claims;角色取自账户读取结果。
|
||
|
||
`ACCOUNT_READER` 已替换为真实 Prisma 读取;数据库故障仍返回 503。微信登录、本人摘要和有限 reason 已接入;账户资料修改、注销及主动版本递增服务仍待实现。小程序恢复逻辑和 Web 内容继续作为后续目标设计。
|
||
|
||
## 3. 身份、凭证与权限分层
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
WX[微信 code] --> Verify[服务端验证微信身份]
|
||
Verify --> Account[users.id · Doodle 账户]
|
||
Account --> Mini[小程序 Bearer JWT]
|
||
Account -.后续.-> Web[Web Cookie 会话]
|
||
Mini --> Check[当前账户状态与版本]
|
||
Web --> Check
|
||
Check --> Permission[角色及资源归属检查]
|
||
Permission --> Business[收藏 · 下载 · 资料 · 管理]
|
||
```
|
||
|
||
| 标识 | 用途 | 不承担的职责 |
|
||
| ------------- | -------------------------------------- | ---------------------------------------- |
|
||
| `users.id` | 全端稳定业务主键,业务关联及审计使用 | 不作为秘密或登录凭证 |
|
||
| 小程序 OpenID | 当前配置的小程序内识别微信身份 | 不假设与网站、公众号的 OpenID 相同 |
|
||
| UnionID | 同一微信开放平台账户下的关联线索,可空 | 首期不唯一、不自动合并账户 |
|
||
| `session_key` | 微信数据加解密需要的服务端密钥 | 不作为 Doodle Bearer,不下发客户端 |
|
||
| Doodle JWT | 小程序向本站 API 出示的访问凭证 | 不固化角色,不包含微信身份和用户资料 |
|
||
| 客户端 UUID | 现有埋点或本地安装标识 | 不用来认证、授权、找回账户或证明设备归属 |
|
||
|
||
服务层只接收经过守卫构造的可信用户上下文。公开 DTO 和客户端请求不能写入 `role/status/authVersion/openid/unionid`;即使请求者是管理员,资料更新接口也不接受这些字段。
|
||
|
||
## 4. 小程序登录流程
|
||
|
||
### 4.1 接口契约
|
||
|
||
所有路径省略 `/api/v1`。`POST /auth/wechat/login` 显式公开,仍受参数校验和限流保护。
|
||
|
||
```json
|
||
{
|
||
"code": "本次 wx.login 返回的临时凭证",
|
||
"mode": "resume"
|
||
}
|
||
```
|
||
|
||
`code` 必须为非空、有长度上限的字符串;不接受客户端 AppID、AppSecret、OpenID、userId 或角色。`mode` 必填,只有以下两种值:
|
||
|
||
- `resume`:只恢复现有账户,不创建用户。已有登录记录的启动、token 到期及 401 恢复都用它;身份对应账户已删除时返回 `ACCOUNT_NOT_FOUND`。
|
||
- `start`:允许首次建号。全新安装、没有任何账户历史时可以非阻塞静默调用;本机曾注销或发现账户被删除时,必须由用户再次点击“开始使用账户”后调用。
|
||
|
||
`mode` 是客户端的创建意图,不是安全凭证。两个模式都先验证微信身份、都拒绝 disabled 账户;`start` 不允许重置现有账户、解除禁用或选择目标 userId。
|
||
|
||
成功返回:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"accessToken": "Doodle JWT",
|
||
"tokenType": "Bearer",
|
||
"expiresIn": 86400,
|
||
"expiresAt": "2026-10-16T02:00:00.000Z",
|
||
"user": {
|
||
"id": "沿用旧用户 ID 或新生成的 ID",
|
||
"nickName": null,
|
||
"avatarUrl": null,
|
||
"avatarUrlExpiresAt": null,
|
||
"createdAt": "2026-10-15T02:00:00.000Z"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
不返回 `openid/unionid/session_key/authVersion` 或完整 users 记录。`avatarUrl` 非空且为私有签名地址时同时返回期限。统计走已有数量接口,不在每次登录中计数;小程序适配层将 `id` 转为现有页面需要的 `_id`,移除 UserInfo 中的微信身份字段。管理入口若需要角色,用受保护的 `GET /me` 获取当前 `role`,仅用于显示。
|
||
|
||
登录及本人资料响应设置 `Cache-Control: no-store`,不进入 CDN/公共响应缓存。JWT 示例中的期限取自实际签发结果,不能另算出与 `exp` 不一致的期限。
|
||
|
||
### 4.2 服务端步骤
|
||
|
||
1. 校验参数及入口限流,用服务端固定的小程序配置调用微信 `code2Session`;外部网络调用不放在数据库事务或行锁内。
|
||
2. 验证 HTTP/响应格式、微信错误码和非空 OpenID;成功时 `errcode` 可以不出现,出现则必须是成功值。UnionID 空串规范为 NULL。当前没有加解密用途,`session_key` 不落库、不缓存、不记录日志。
|
||
3. 开启短事务,按可信 OpenID 查询用户。存在则锁定用户并核对 active;不存在且 mode=resume 则结束并返回 `ACCOUNT_NOT_FOUND`;mode=start 才插入新账户。
|
||
4. 新账户由服务端生成 ID,默认 `role=user/status=active/authVersion=1`。用 `UNIQUE(openid)` 和明确的冲突处理兜底,并发失败时重新读取实际账户;不能覆盖其角色、状态、资料或版本。PostgreSQL 唯一冲突会中止事务,须用 `ON CONFLICT` 或回滚后的新事务重读,不能直接在失败事务里继续查询。
|
||
5. 已有账户更新 `last_active_at`;本次 UnionID 缺失不清空旧值,有值且旧值为空才补齐;两个非空值不一致时记录脱敏异常供核查,不覆盖、不合并。实际资料变化才维护相应 `updated_at`,不把普通登录当成权限或资料修改。
|
||
6. 提交后按本次确认的 `id/authVersion` 签发 JWT,显式构造 DTO。若提交后恰好发生禁用、注销或版本递增,旧快照 token 会在下一次账户核对时失效,不能跳过检查以迁就它。
|
||
|
||
被禁用的账户不能通过再次登录变成新用户。旧用户是否存在取决于已完成的数据导入;切换前必须迁入旧 users,不能先对空生产库开放 start 模式,再补导产生重复账户。
|
||
|
||
### 4.3 微信接口调用约束
|
||
|
||
微信小程序 code 有效期五分钟且只能使用一次;每次登录尝试现取现用。提交后超时也可能已经被微信消费,不使用同一个 code 盲目重试;客户端需要恢复时重新调用 `wx.login`,并受统一重试次数限制。[微信 wx.login](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html)、[小程序登录说明](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html)。
|
||
|
||
| 配置/场景 | 建议行为 |
|
||
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
|
||
| AppID/AppSecret | 仅服务端配置;新增 `WECHAT_MINI_APP_ID/WECHAT_MINI_APP_SECRET` 的启动校验,禁止读取客户端配置作为身份来源 |
|
||
| 外部 HTTP | 固定微信 HTTPS 地址,校验 TLS,建议单次超时 5 秒;不跟随到任意地址,不记录带 secret/code 的 URL |
|
||
| 微信 40029 或已使用 code | 返回 400 + `WECHAT_CODE_INVALID`;一次恢复周期内最多额外获取一次新 code |
|
||
| 微信 40226 | 返回 403 + `WECHAT_LOGIN_BLOCKED`;停止自动登录,不能伪装成普通 code 过期 |
|
||
| 微信 45011 / 本站限流 | 返回 429 + `RATE_LIMITED`;遵守 `Retry-After`,不立即循环 |
|
||
| 微信繁忙、超时、响应异常 | 返回 503 + `AUTH_UPSTREAM_UNAVAILABLE`;退避,允许公开功能继续使用 |
|
||
| AppID/AppSecret 配置失效 | 返回安全的服务不可用提示并告警;不引导客户端不断获取 code |
|
||
| 本站数据库失败 | 返回 503;不能改用客户端身份或只验 JWT 放行 |
|
||
|
||
具体错误映射以[微信 code2Session](https://developers.weixin.qq.com/miniprogram/dev/server/API/user-login/api_code2session.html)为准,不把微信原始 errmsg/响应直接下发。建议起步使用入口 IP 限流,例如单 IP 每分钟 30 次并保留小突发;共享网络需实测调参,不能只靠它。获得可信 OpenID 后补身份维度节流,成功登录不因客户端重试重复建号。首期单实例可用有界内存计数,重启会重置;多实例或严格配额时再选共享存储。
|
||
|
||
微信 `wx.checkSession` 检查微信的 `session_key`,不能检查 Doodle JWT。当前不使用微信加解密能力,不将它加入每次 API 请求流程。[微信 wx.checkSession](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.checkSession.html)。
|
||
|
||
## 5. 凭证、账户检查与业务授权
|
||
|
||
### 5.1 JWT
|
||
|
||
沿用现有 HS256、`iss=doodle-store`、`aud=doodle-mini`、`sub=userId`、`authVersion/iat/exp`,默认 `JWT_EXPIRES_IN_SECONDS=86400`。JWT 是可读取的签名载荷,里面不放用户资料、微信身份、手机号或秘密。
|
||
|
||
不提供任意 userId 换 token、前端自行续签、仅凭旧 token 换新 token 的接口。普通登录、更新资料、清资料不递增 `auth_version`,避免其他设备无故失效。
|
||
|
||
每次受保护请求顺序固定:
|
||
|
||
1. 从 Authorization 读取 Bearer,验证签名、算法、issuer、audience、期限及字段。
|
||
2. 真实 AccountReader 只查询当前 `id/status/role/authVersion`,核对用户存在、active、版本相同;数据库不可用则拒绝服务。
|
||
3. 将结果写入 `request.auth`,执行路由角色检查。
|
||
4. 业务服务检查目标归属和发布状态;写事务锁用户行后重新核对状态、版本,以及需要的管理员角色,再按数据库规定锁题型/内容。
|
||
|
||
第一阶段不缓存账户状态,确保禁用、降权、注销可在后续请求立即生效。读请求已通过检查后发生的并发状态变更,不宣称能撤回已发出的响应;关键写入与禁用/注销事务通过同一账户行锁串行化。管理事务也要检查操作者,不能只在控制器检查一次角色。
|
||
|
||
### 5.2 权限矩阵
|
||
|
||
| 接口/操作 | 未登录 | active 普通用户 | active 管理员 |
|
||
| ---------------------------------------------- | -------------- | -------------------------------- | -------------------------------------- |
|
||
| 首页、分类、active 功能及 active 内容列表/详情 | 允许 | 允许 | 允许,公共接口仍只看发布内容 |
|
||
| 登录 | 验证微信后允许 | 允许 | 允许,不因登录自动授权 |
|
||
| `/me`、收藏、下载历史及数量 | 拒绝 | 仅本人 | 仅本人 |
|
||
| 上传头像、修改/清除资料、注销 | 拒绝 | 仅本人 | 仅本人 |
|
||
| 管理查询、发布、上下架、配置、统计修订 | 拒绝 | 拒绝 | 明确管理路由允许,并按范围审计 |
|
||
| 读取其他用户私有头像、历史或收藏 | 拒绝 | 拒绝 | 默认拒绝;admin 不是全量私有数据通行证 |
|
||
| 未来服务端生成/私有文件 | 按专门规则 | 登录后仍核对配额、归属、资源状态 | 仍按对应业务规则 |
|
||
|
||
公开接口用 `@Public()` 明确声明,且不返回本人收藏状态;需要个人状态时单独请求 `/me/...`。现有 Public 装饰器会完全跳过守卫,不将它当作“有 token 就自动解析身份”的可选鉴权模式。
|
||
|
||
资产接口先验证 purpose、大小、格式、owner 和状态;头像按用户确认使用公开空间,只允许绑定本人拥有、用途为 avatar 且 ready 的资产。客户端传来的 assetId、worksheetId、resourceId 都必须做业务校验,不能仅因用户已登录便接受。
|
||
|
||
### 5.3 管理权限与版本撤销
|
||
|
||
首个管理员通过受控初始化脚本将已验证的现有账户升为 admin,不能按“第一个登录者”授权,也不能依赖 develop 环境。授权、降权、禁用、重新启用采用专用服务/脚本,在同一事务锁操作者与目标用户,按 ID 排序获取同层锁,写入审计;初始化脚本使用 system 操作者。
|
||
|
||
建议上述角色/状态变更都递增目标 `auth_version`,防止恢复 active 或重新授权后,早先的 token 再次恢复权限。仅撤销全部凭证时同样递增版本。避免整数溢出,变更失败不绕回旧版本。
|
||
|
||
首期没有独立 sessionId,因此不承诺单设备远端退出。小程序暂不增加普通“退出登录”按钮;本地清除缓存不等于服务端撤销凭证。若后续需要单设备退出、设备列表或管理员独立登录期限,再引入会话记录。未来后台的专用登录态还必须验证 `active+admin`,普通用户不能获得后台会话。
|
||
|
||
## 6. 小程序状态管理与 401 恢复
|
||
|
||
### 6.1 状态与缓存
|
||
|
||
`auth.ts` 对页面保留简单入口,但改变其内部含义:
|
||
|
||
| 方法 | 目标职责 |
|
||
| ----------------------- | ------------------------------------------------------------------------- |
|
||
| `getCachedUser()` | 只用于首屏展示,不能表示已经通过服务端认证 |
|
||
| `ensureAuthenticated()` | 获取尚未到期的凭证,或共用登录 Promise 完成微信登录 |
|
||
| `getUser()` | 兼容现有调用:先确保凭证,再返回当前用户摘要;不再仅凭 7 天缓存认定已登录 |
|
||
| `refreshUser()` | 已登录时 `GET /me` 刷新资料,必要时走一次正常认证恢复;不先清凭证 |
|
||
| `updateUserProfile()` | 请求受保护资料接口,成功后同步本地摘要和 globalData |
|
||
| `clearUser()` | 仅作为内部清理工具;明确清理原因,不能顺手触发重新注册 |
|
||
|
||
建议用带版本、环境及 AppID 命名空间的单个 Storage 对象保存 `{accessToken, expiresAt, user}`,避免多个 key 写入一半;读取时校验结构,旧 `user_info` 只可辅助展示,升级后移除。资料缓存不保存微信身份;角色缓存最多影响按钮显示。
|
||
|
||
凭证先内存、再 Storage;在到期前约 60 秒按需重新登录,`expiresIn` 从收到响应时估算本机剩余期,不依赖设备绝对时间完全准确。最终有效性始终由服务端判断;客户端若解析 JWT 只为时间提示,不能据此授权。上传/请求失败日志不得输出完整 token 或登录响应。
|
||
|
||
另存轻量的账户使用状态:是否曾建立账户、是否主动注销/暂停恢复。注销成功后即使 user 缓存被删除,也保留“需用户主动重新开始”的标记;否则下一次 `onLaunch` 会把它误认作全新安装。卸载清数据后该标记可能消失,此时重新验证微信可以建新账户;本方案不提供跨卸载的禁止再注册策略。
|
||
|
||
### 6.2 自动恢复规则
|
||
|
||
1. 公共请求不附带凭证、不启动登录。应用启动可非阻塞预热登录;失败不影响首页和已有本地生成/保存。
|
||
2. 受保护请求获取当前 token,记录发送时的 token 代次和业务所属 userId。登录请求直接使用底层 HTTP,不经过需登录的包装,避免递归。
|
||
3. 收到可恢复的 401 时,若已有较新 token,直接使用新 token 重试;不能让旧请求迟到的 401 清掉新凭证。否则共用唯一登录 Promise,以 mode=resume 恢复。
|
||
4. 一次原始业务请求只允许一次认证恢复和一次重发;恢复登录若遇到明确无效 code,可以额外获取一次新 code,之后停止。第二次业务 401、403、429、5xx 或网络错误不触发新的自动登录链。
|
||
5. 恢复后必须仍是原 userId,才能重发原业务写入。账户改变则中止、清理相关视图,由用户重新操作;下载补报队列绑定原 userId,不能代报到新账户。
|
||
6. GET、按目标定义的 PUT/DELETE 收藏可安全重发;下载使用原 exportId,资料更新复用同一内容。管理发布、通用 POST、资产上传、注销等不可通用重放,只由业务层按幂等键或专门流程恢复。超时不是“未提交”,不对未知结果的写入盲目重试。
|
||
7. 清理/注销时增加本地状态代次,使尚未结束的登录、资料及列表响应不能重新写回旧状态;等待中的业务请求同时取消或忽略,Promise 结束后只清理自身对应实例。
|
||
|
||
短时间登录失败保留退避状态,例如 5 秒、15 秒、60 秒并设置上限;页面并发请求共用同一次失败结果,不各自重新登录。网络恢复或用户点击重试可重新尝试;公开功能不受阻。
|
||
|
||
### 6.3 错误契约
|
||
|
||
保留 `{code:HTTP状态码, message, requestId}`,增加可选的枚举 `reason`;先更新服务端错误过滤器和契约,再接小程序。只允许预定义安全错误,不透传数据库异常或微信原始消息。
|
||
|
||
| HTTP / reason | 小程序行为 |
|
||
| --------------------------------------------- | --------------------------------------------------------------- |
|
||
| 401 `AUTH_REQUIRED/AUTH_EXPIRED/AUTH_INVALID` | 有账户恢复意图时合并静默恢复一次;匿名首次用户按 start 流程处理 |
|
||
| 401 `AUTH_REVOKED` | 清凭证并停止后台恢复;用户主动继续时重新验证微信,不能循环续签 |
|
||
| 401 `ACCOUNT_NOT_FOUND` | 清旧账户数据,停止自动建号;公开功能可用,用户主动开始才 start |
|
||
| 401 `ACCOUNT_DISABLED` | 清凭证并停止自动恢复;提示账户不可用,再次验证也不解除禁用 |
|
||
| 403 权限不足或微信登录被拦截 | 提示无权限/无法登录,不清正常资料后反复尝试 |
|
||
| 400/409 | 提示参数/业务冲突;不通过重登处理 |
|
||
| 429 | 遵守重试期限;请求层透传安全的 `Retry-After` 信息 |
|
||
| 503/网络错误 | 保留可用显示缓存,标明暂不可用;不返回伪造的空列表或零统计 |
|
||
|
||
认证错误原因只在已验签后的必要账户检查中细分,不能做成按客户端任意 userId 查询账户存在/状态的接口。未知 reason 保守停止自动恢复。
|
||
|
||
`wx.uploadFile` 也要附带凭证并解析 HTTP 状态;先确保登录再上传,不将 multipart 上传纳入通用自动重发。下载签名 URL 本身是短期访问能力,不把本站 Bearer 附给七牛或任意外部域名;私有 URL 到期后重新向受保护 API 获取地址。
|
||
|
||
## 7. 资料、清除、注销与重新进入
|
||
|
||
### 7.1 资料接口
|
||
|
||
| 接口 | 行为 |
|
||
| -------------------- | --------------------------------------------------------------------------------- |
|
||
| `GET /me` | 返回本人公开 DTO 及显示需要的当前 role;数量按独立接口查询;不输出 OpenID/UnionID |
|
||
| `PATCH /me/profile` | 仅允许 `nickName/avatarAssetId`;字段缺省是不修改,NULL 是清除;拒绝未知字段 |
|
||
| `DELETE /me/profile` | 清昵称及头像引用,保留账户、凭证、收藏、历史;前端更新摘要,不清登录态 |
|
||
| `DELETE /me` | 当前凭证 + 新微信 code + 用户确认,验证同一身份后按数据库规则完整注销 |
|
||
|
||
头像流程:`chooseAvatar` 得到临时本地文件 → 带 Bearer 上传 `POST /me/avatar-upload` → 得到本人 avatar assetId → `PUT /me/profile` 绑定。服务端校验归属,不能接受任意头像 URL、文件路径或他人 assetId。替换/清除后按既有文件清理机制处理旧头像对象,失败重试,不在数据库事务里请求对象存储。
|
||
|
||
昵称按数据库上限 100 个 Unicode 字符校验,首尾空白规范化,不把它作为唯一账号名。个人中心可分别保存昵称和头像,不强迫二者齐全;微信填写组件提供的临时路径不是永久头像地址。[微信头像昵称填写](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html)。
|
||
|
||
### 7.2 注销的完整边界
|
||
|
||
注销请求体建议为 `{code, confirm:true}`。前端在明确确认后现取 code;后端调用微信校验得到可信 OpenID,必须对应当前认证账户,不调用会自动建号的登录方法。code 不能替换 JWT 身份,也不能指定要删谁。
|
||
|
||
数据库步骤沿用已定规则:锁用户并重查版本/状态 → 按 ID 锁涉及的题型,仅扣功能收藏贡献 → 删除功能和内容两类收藏及下载记录 → 解除头像/私有资产关联并标记清理 → 去除管理审计中的身份及个人信息关联 → 删除用户,同一事务提交。之后所有旧 JWT 因查不到用户而失效。
|
||
|
||
客户端收到成功后清凭证、资料、本人列表和下载补报队列,阻止在途结果回写,记录暂停建号状态,回到可匿名浏览的首页。其他设备的 resume 登录查不到用户时也停止自动建号。用户主动重新开始时才创建全新 userId,旧收藏和历史不恢复。
|
||
|
||
若注销响应丢失,客户端进入“确认结果中”状态,暂停自动 start、原写请求及补报。可用原 token 查询 `GET /me`:`ACCOUNT_NOT_FOUND` 说明原账户已不存在,应执行本地清理;仍存在则让用户明确重试并获取新 code;网络失败则保持未知,不能当作成功或重新建号。注销接口不进入通用 401 重试器。
|
||
|
||
本方案通过 mode=resume 和客户端意图管理阻止无意重新注册,不承诺删除后永远禁止该微信再建号;跨设备的主动 start 仍可创建新账户。若未来要求注销冷静期或全局禁止再注册,需要另设计受控的身份保留策略,不能只靠已删除的 users 行实现。
|
||
|
||
## 8. 旧版本兼容与上线
|
||
|
||
旧版没有 `wx.login` code 或本站 JWT,兼容云函数仍从 `cloud.getWXContext()` 获取可信身份,经已有迁移方案规定的服务间签名入口访问同一业务服务。
|
||
|
||
- 兼容桥只允许已登记的小程序 AppID、函数/action 白名单、时间窗及防重放校验;客户端 event 中的身份一律不可信。
|
||
- 内部桥不伪装成普通 Public 业务路由。即使声明跳过 JWT,也必须由独立桥接守卫验签,再执行真实账户状态和服务层权限检查;签名密钥不能用于普通 JWT 签发。
|
||
- 旧登录 action 可以按既定兼容语义首次建号;其他收藏、下载、资料、设置 action 只查现有账户,不因账户已删而自动创建。桥接 action 和旧响应形状按白名单转换,不返回完整数据库模型。
|
||
- 旧版主动调用登录仍可能在注销后新建号,这是已发布代码的兼容限制;新用户 ID 不恢复旧数据。完整的客户端暂停建号体验只由新版保证,旧版下线期限按迁移观察确定。
|
||
- 历史资料响应如确有兼容字段需要,可在受控桥内返回已核实的必要字段;新版 DTO 不延续 OpenID/UnionID 暴露。
|
||
- 兼容桥仍需检查 admin,不能把“来自云函数”当成管理授权;旧管理页面要求使用新版。签名防重放记录必须原子且有清理期限,具体存储在兼容桥实施时落地。
|
||
|
||
上线顺序:先完成受控试迁与旧身份对账,再部署 API/兼容桥,在切换窗口统一写入源,最后发布新版。开发、体验、正式 API/缓存命名空间隔离;不得把不可信 envVersion 当成后端权限或测试库选择依据。体验版如仍对应同一 AppID,必须核实数据隔离和资源隔离策略。
|
||
|
||
## 9. 未来 Web:共用账户,独立管理会话
|
||
|
||
### 9.1 推荐首条路径:现有小程序扫码确认
|
||
|
||
Web 面向现有微信用户提供“使用涂鸦丫小程序登录”:网页展示带短期 challenge 的小程序码 → 微信扫码进入小程序登录确认页 → 小程序用当前有效 Doodle 身份明确确认 → 原网页兑换自己的 Web 会话。
|
||
|
||
这是项目自行实现的登录确认协议,不是直接调用网站应用微信 OAuth。它能直接使用已经验证的小程序 userId,不需要用两个 OpenID 猜测是否同一人;未来即使 UnionID 为空也能共用收藏和历史。微信官方提供小程序码进入指定页面的能力,登录 challenge、安全校验和浏览器会话由本站实现。[获取小程序码](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/qr-code.html)。
|
||
|
||
未来实现必须包含:
|
||
|
||
1. 网页创建挑战时建立 HttpOnly 的预登录浏览器绑定,服务端保存绑定摘要;二维码只携带随机定位符,不包含 token、userId 或浏览器兑换秘密。
|
||
2. challenge 建议 2 分钟过期,随机数至少 128 位,区分 user-web/admin 用途。小程序码图案本身可以长期存在,是否过期以本站 challenge 为准;过期扫码必须拒绝。
|
||
3. 确认页展示“将登录涂鸦丫网页”、使用的账户及双方可核对的短码,要求主动确认。扫码/打开页面本身不能完成登录,防止误扫和远程诱导登录。
|
||
4. 只有持有有效小程序凭证者可确认;只有创建挑战的浏览器绑定可查询/兑换结果。扫码定位符不能作为领取网页会话的凭证。
|
||
5. 状态原子流转 `pending → confirmed → consumed`,另有 rejected/expired;一次挑战只能成功兑换一次。兑换时重新核对用户状态和确认时的 authVersion,禁用/注销/撤销后不能再兑换。
|
||
6. 登录后轮换预登录标识,创建全新的服务端会话;轮询只取状态,不返回 Bearer;会话通过 `Set-Cookie` 交给原浏览器,不能经 URL、二维码或小程序转传。
|
||
|
||
此路径需要新增短期挑战存储,Web 上线时优先用 PostgreSQL 表和到期清理,不为低频轮询先引入 Redis/WebSocket。小程序码生成还需要微信服务端调用凭证管理,这与用户登录 JWT 不同,届时单独接入。
|
||
|
||
它主要解决电脑 Web 登录,不能覆盖所有手机浏览器或非微信用户。移动 Web、独立账号和非微信人群有实际需求后,再增加相应入口。
|
||
|
||
### 9.2 另一条路径:网站应用微信 OAuth
|
||
|
||
如果希望网页直接使用微信网站登录,可接入已审核的网站应用及其 AppID/AppSecret,使用授权码回调、服务端换身份和随机 state 验证。小程序 `wx.login` code 与网站授权 code 不能混用,微信 OAuth 的 access_token 也不是 Doodle API token。[网站应用微信登录开发指南](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)。
|
||
|
||
新增应用身份前,才引入 `auth_identities`:建议包含 `id/user_id/provider/app_id/subject/union_scope/union_id/created_at`,其中 `UNIQUE(provider, app_id, subject)`,subject 对应该应用可信 OpenID;`union_scope` 标明可信的开放平台账户范围,不能仅比较无作用域字符串。
|
||
|
||
届时的结构迁移顺序:
|
||
|
||
1. 将现有 `users.openid` 按服务端已核实的唯一小程序 AppID 回填成身份记录,逐项校验,保留原 userId。
|
||
2. 小程序和新增应用统一通过身份表找账户,稳定后再将 users 的旧身份列改为可空/过渡字段并停止作为权威源;最终移除。不能让只有网站身份的新账户仍被迫填写小程序 OpenID。
|
||
3. 已绑定身份直接进入原账户。新身份即使拿到相同 UnionID,也只作为绑定候选;按本项目规则要求用户显式验证并绑定,不自动合并已有账户。
|
||
4. 绑定必须有当前账户登录态 + 新身份的独立有效证明,不能由客户端上报 UnionID。身份已属于其他 userId 时返回冲突,不自动搬移收藏/历史;账号合并另立流程。
|
||
5. 注销时事务删除所有登录身份及会话。多个身份的唯一冲突和并发绑定必须依赖数据库约束,不能先查一次就认定安全。
|
||
|
||
UnionID 在同一微信开放平台账户范围内关联用户,且可能缺失;不是所有微信产品的全局账号 ID。[UnionID 机制](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html)。当前不恢复刚移除的 users.mini_app_id,也不提前改动已完成的 13 表基线。
|
||
|
||
### 9.3 Web 会话与浏览器安全
|
||
|
||
推荐 Web 与 API 通过同源 `/api` 反向代理访问,由 API 直接管理不透明会话 Cookie:`Secure; HttpOnly; SameSite=Lax; Path=/`,使用 `__Host-` 前缀且不设 Domain。不把长效凭证放在 localStorage、页面脚本变量、URL 或日志中。[OWASP 会话管理指南](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)。
|
||
|
||
Web 阶段新增 `auth_sessions`,保存随机会话秘密的摘要,而不是原始秘密;建议字段为 `id/token_hash/user_id/client_type/auth_version/created_at/last_seen_at/idle_expires_at/absolute_expires_at/revoked_at`。每次会话请求核对期限、撤销状态、账户状态和版本,角色仍取当前 users;注销清理会话,单浏览器退出只撤销当前会话。
|
||
|
||
普通 Web 初始可取 7 天空闲期限、30 天绝对期限;后台单独取 30 分钟空闲期限、8 小时绝对期限,并重新核对 active+admin。这些是未来上线时需要实测确认的起步值,不修改当前小程序 JWT 配置。
|
||
|
||
Cookie 接入时需要明确调整现有 Bearer-only 守卫:先按路由允许的凭证类型解析为统一用户上下文,再共用账户/版本/角色及归属检查。业务 API 可允许小程序 Bearer 或 Web session,专用后台登录/会话只接受 admin 用途;存在相互冲突的双凭证时拒绝,不在 Bearer 失效后悄悄改用另一身份 Cookie。小程序 audience 继续限定,不直接把 `doodle-mini` JWT 复制到网站。
|
||
|
||
Cookie 写请求必须有 CSRF 防护,建议 Origin 校验 + 与会话绑定的 CSRF token;OAuth state 和扫码 challenge 还需独立绑定原浏览器。SameSite、CORS、HttpOnly 都不能单独替代这些检查。只读 GET 不执行写操作;公开页面不夹带个人响应缓存。登录提升权限后更换会话标识,明确退出时撤销服务端会话。
|
||
|
||
当前 CORS 是 `credentials:false`。同源代理下不必为 Cookie 开放跨源;若未来必须跨源,需精确 Origin 白名单、credentials、Cookie SameSite/第三方 Cookie 限制及 CSRF 一并重新设计,不能只翻一个开关。前端框架和域名拓扑到 Web 开发时确定。
|
||
|
||
## 10. 数据库影响、落点与实施顺序
|
||
|
||
### 10.1 本次数据库无需变更
|
||
|
||
| 需求 | 当前结构是否足够 | 何时新增结构 |
|
||
| -------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
|
||
| 单小程序登录、本人资料与权限 | 足够,users 的 OpenID 唯一约束、role/status/auth_version 已具备 | 当前无需 DDL |
|
||
| 全账户撤销、禁用及降权 | 足够,变更版本并实时读账户 | 当前无需 DDL |
|
||
| Web 小程序扫码登录 | users 足够,需挑战和浏览器会话存储 | Web 上线时增加 challenge/session 表 |
|
||
| 单设备远端退出、设备管理 | 当前不足 | 真实需求出现时引入会话标识及服务端会话检查 |
|
||
| 网站 OAuth、手机号、邮箱等多身份 | 当前 users.openid NOT NULL 不足 | 引入身份表并迁移 users 身份约束,不提前塞空串或伪 OpenID |
|
||
|
||
新表将来通过新的增量迁移接入,不重跑已有基线;身份/会话表进入正式数据库设计后,再实现相应能力。
|
||
|
||
### 10.2 代码落点
|
||
|
||
- `doodle-store/apps/server/src/auth`:扩展现有模块,负责登录控制器、微信客户端、JWT、账户读取、限流及认证错误;不再在 modules 下重复建第二套 auth。
|
||
- `doodle-store/apps/server/src/modules/users`:本人资料、资产绑定、清资料、注销及账户状态/角色变更,复用事务规则。
|
||
- `doodle-store/packages/contracts`:公开请求/响应、有限错误原因和用户 DTO;不包含 Prisma 模型、微信密钥或私有身份记录。doodle-mini 按 HTTP 契约接入,不跨仓库导源码。
|
||
- `doodle-mini/miniprogram/utils/auth.ts` 与业务 HTTP 层:缓存/凭证/状态机、并发合并、401 恢复;页面调用业务方法。
|
||
- 小程序个人中心、设置、收藏/历史、上传及现有管理入口:分别适配;不把迁移理解为只替换一个函数名。
|
||
|
||
### 10.3 建议推进顺序
|
||
|
||
1. Prisma schema/既有 SQL 基线登记、真实 AccountReader;将已有守卫测试接到真实账户查询。
|
||
2. 微信客户端与登录 API、resume/start 语义、显式 DTO、错误 reason、限流与日志脱敏。
|
||
3. 小程序统一认证请求层、缓存升级、并发/迟到响应处理;先打通体验版 `wx.login → API → GET /me`。
|
||
4. 本人资料、头像、清资料和注销;接入收藏/下载及幂等恢复,再补管理接口的事务内权限核对。
|
||
5. 兼容桥、迁入老身份样本、真机/离线/过期/注销验证,按迁移任务清单切换。
|
||
6. Web 实际启动时实现扫码挑战、Cookie 会话和对应数据库增量;新增网站身份时才迁移身份表。
|
||
|
||
真实微信联调前需核实 AppID/AppSecret、开放平台绑定情况、HTTPS 及 request/upload/download 合法域名、体验版数据隔离。现在无需把密钥写入设计文档,也不以这些待核实项阻塞方案评审。
|
||
|
||
## 11. 实施验收清单
|
||
|
||
以下是分阶段验收要求;已实施部分及自动验证范围见配套接入说明,不能将服务端测试视为小程序真机/Web 验收。
|
||
|
||
| 场景 | 预期结果 |
|
||
| ----------------------------------------- | --------------------------------------------------------------------------------------- |
|
||
| 老用户有效 code 登录 | userId 与旧 `_id` 相同,收藏/历史不断联 |
|
||
| 同 OpenID 并发首次 start | 只有一个 users,唯一冲突处理不覆盖权限/资料 |
|
||
| resume 查不到用户 | 不建号,返回 ACCOUNT_NOT_FOUND;需主动 start |
|
||
| 客户端伪造 AppID/OpenID/userId/role | DTO 拒绝或不作为可信身份,不影响目标账户 |
|
||
| code 无效/重复/过期、微信风控/超时/限流 | 有限且正确的错误映射,不创建用户、不无限重试 |
|
||
| JWT 伪造、过期、错误 audience/issuer/算法 | 拒绝;不接受无期限载荷或 URL token |
|
||
| token 有效但数据库不可用 | 503,不只验签放行 |
|
||
| 账户禁用、降权、撤销后重新启用 | 旧凭证仍无效;重新验证也不能绕过 disabled |
|
||
| 多个请求同时 401、旧 401 迟到 | 只发生一轮恢复;不会清除新凭证,每个业务最多重发一次 |
|
||
| 清缓存/注销与登录、列表请求并发 | 旧响应不复活状态;注销后后台 resume 不重新建号 |
|
||
| 重新登录返回不同 userId | 原写请求不重发,原下载队列不代报 |
|
||
| 保存成功后上报超时、重试 | 原 exportId 重用,不重复计数、不重复扣本地配额 |
|
||
| 清资料 | 收藏/历史及有效凭证保留,头像对象进入清理流程 |
|
||
| 注销成功或响应丢失 | 事务完整,旧 token 失效;未知结果可确认,不自动重放或重新注册 |
|
||
| 任意用户改他人头像、读他人历史 | 拒绝;管理员默认也不能读取全量私有数据 |
|
||
| 小程序本地改角色或显示管理按钮 | 管理 API 仍拒绝,管理写事务重新核对操作者 |
|
||
| 日志/接口/缓存检查 | 无 AppSecret、session_key、code、完整 token、原始微信身份泄漏 |
|
||
| iOS/Android 体验版、网络断开/恢复 | 正常静默登录;离线公开内容和本地生成可用,远端错误不显示成空数据 |
|
||
| Web 后续验收 | 挑战过期/重放/误扫/跨浏览器兑换拒绝;Cookie/CSRF/退出/账户撤销有效;与小程序同一 userId |
|
||
|
||
## 12. 参考与待确定事项
|
||
|
||
代码和数据库规则以本文引用的仓库文件为准。微信资料于 2026-10-15 查阅;未来接入时复核平台准入、配额及接口变化。
|
||
|
||
- [旧小程序登录方案](../../../doodle-mini/docs/用户登录与信息授权方案.md):仅作为云开发历史背景,实际行为以代码为准。
|
||
- [迁移任务清单](./迁移任务清单.md):真实开发、数据迁移与切换验收入口。
|
||
- [微信 code2Session](https://developers.weixin.qq.com/miniprogram/dev/server/API/user-login/api_code2session.html)、[UnionID](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html):身份来源与作用域。
|
||
- [微信网站应用登录](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)、[OWASP 会话管理](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html):未来网站登录和浏览器会话的参考,不代表本站已接入。
|
||
|
||
本次建议默认按“小程序 24 小时 JWT、暂不增加表、Web 优先现有小程序扫码确认”推进。Web 的移动端覆盖、网站应用准入及实际域名拓扑留到 Web 启动时确定;若改选独立手机号/邮箱登录,需要补充验证、凭证、找回和解绑方案,不能直接复用昵称或客户端 UUID。
|
||
|
||
## 小程序首期接入进度(2026-10-19)
|
||
|
||
小程序已接入 `wx.login → POST /auth/wechat/login`,启动/回前台非阻塞预热;`utils/auth.ts` 提供 `getUser`、`ensureAuthenticated`、`getCachedUser`、`refreshUser` 和内部清理入口。`refreshUser` 调用受保护的 GET /me。当前阶段没有将资料更新伪装成已迁移:资料编辑、头像上传、收藏、下载记录及账户设置仍由独立的 `utils/legacy-auth.ts` 兼容旧云业务,后续逐项切换。
|
||
|
||
新凭证通过 AppID 与完整 API baseUrl 隔离,保存版本化的账户历史、凭证、本机预计到期时间及白名单用户摘要。公开分类/题型请求显式不鉴权;其余请求默认获取并携带本站 JWT。首次登录、提前 60 秒续期和 401 恢复共用 Promise;一次业务请求最多恢复与重发一次,迟到的旧 401 复用较新凭证,恢复后账户改变则停止原请求。无效微信 code 最多重新获取一次,网络错误不会复用 code 或自动循环。
|
||
|
||
账号不存在/禁用会清理凭证并暂停自动恢复,账户历史标记不随会话清理丢失;个人中心提供用户主动重新开始的入口,服务端仍负责拒绝禁用账户。新后端摘要使用 `globalData.user`,旧云端使用 `globalData.legacyUser`,不混用两种用户 ID 或用旧缓存冒充 JWT。
|
||
|
||
前端专项及分类/预览回归共 23 项通过(真实 8800 联调用例未启用);真实微信 code 兑换需在开发者工具或真机验证。正式部署前仍需配置线上 API 域名并先迁移旧用户及关联数据,避免在未迁移的生产库创建另一套业务身份。
|
||
|
||
## 其余接口切换进度(2026-10-19)
|
||
|
||
已完成微信预拉取之外的云接口替换,旧 `legacy-auth.ts` 兼容层已移除。用户资料、收藏、下载、头像、管理发布/批次与首页动态接口均使用本站用户身份,详见[小程序云接口迁移清单](小程序云接口迁移清单.md)。前文“首期接入进度”是分阶段记录,当前实现以该清单为准;历史云端用户/收藏/下载数据尚未自动搬迁。
|