1. 页面概览
「登录」是 AI 行动平台的统一认证入口(对应前端源码 action/web/src/views/LoginPage.vue)。用户用用户名 + 密码登录后,前端把 JWT token 存入 localStorage 的 aip_token,即可访问各产品受保护路由。本页现支持四种模式:登录、注册(用户名 + 密码,邮箱选填)、忘记密码(提交邮箱或用户名,触发重置邮件)、重置密码(用邮件中的令牌设置新密码,支持 /login?reset_token=xxx 链接直达)。一句话总结:本页是平台唯一登录/注册入口,登录成功即获得全平台访问令牌,忘记密码可自助找回。
2. 访问入口
2.1 路由与菜单
路由 path: /login、name: Login、meta 标题「登录」且 guest: true(公开路由);未登录访问受保护页面时,路由守卫重定向到 /login?redirect=<原路径>,登录后自动跳回。重置邮件中的链接为 /login?reset_token=<令牌>,页面挂载时读取该 query 并直接进入「重置密码」模式。本页无侧边栏菜单,未登录时顶部导航栏不渲染。
2.2 认证与权限
公开页无需 token;已登录(localStorage 有 aip_token)再访问 /login 会被守卫直接跳到 Foundry。登录成功后 token 类型为 JWT(Bearer),存 localStorage['aip_token'],用户名存 localStorage['aip_username']。
2.3 端口与 API 前缀
AIP 后端 18080。登录/注册走 action/web/src/api/client.js(baseURL /api/v1);自助找回的两个端点走 action/web/src/api/aipClient.js(baseURL /aip-api/v1,经 Vite 代理 rewrite 到 AIP 18080),确保请求恒定落到 AIP 而非默认代理目标。
3. 界面布局
居中卡片式,从上到下依次为标题区、提示条、表单区、模式切换区;表单区随模式切换(登录/注册共用一个表单,忘记密码与重置密码为独立表单):
┌──────────────────────────────┐
│ AI 行动平台 │
│ 副标题随模式切换(4 种) │
│ [操作提示条 alert] │
│ ── 登录 / 注册 ── │
│ 用户名 [________________] │
│ 密码 [________________] │
│ 邮箱(选填,仅注册模式)[____] │
│ ── 忘记密码 ── │
│ 邮箱或用户名 [____________] │
│ ── 重置密码 ── │
│ 重置令牌 [________________] │
│ 新密码 [________________] │
│ 确认新密码 [______________] │
│ [ 主按钮(随模式变文案) ] │
│ 模式切换链接(注册/重置/返回登录)│
│ 忘记密码? [自助找回] │
└──────────────────────────────┘
- 标题区:品牌名「AI 行动平台」+ 副标题随模式切换(「请使用您的账号登录」/「创建一个新账号」/「忘记密码 · 自助找回」/「设置新密码」)。
- 表单区:登录/注册表单含用户名、密码(注册多一个选填邮箱);忘记密码表单含「邮箱或用户名」输入框与说明文案;重置密码表单含令牌、新密码、确认新密码(均必填,密码 type=password)。
- 提交区:整行主按钮(
btn-primary btn-block),文案随模式与 loading 变化(登录/注册/发送重置邮件/重置密码)。 - 底部:模式互切链接(「立即注册」/「立即登录」/「去重置密码」/「返回登录」)+ 登录/注册模式下常驻的「忘记密码?自助找回」入口。
4. 交互元素
| 控件 | 位置 | 含义与作用 |
|---|---|---|
| 输入框「用户名」 | 表单(登录/注册) | 登录/注册用户名,必填 |
| 输入框「密码」 | 表单(登录/注册) | 登录/注册密码,必填(type=password) |
| 输入框「邮箱(选填)」 | 表单(仅注册模式) | 注册时可填邮箱;该邮箱是后续自助找回的收件地址 |
| 按钮「登录」/「注册」 | 表单底部 | 提交表单;loading 时文案变「处理中...」并禁用 |
| 链接「立即注册」/「立即登录」 | 表单底部 | switchMode() 切换模式并清空表单与提示 |
| 链接「忘记密码?自助找回」 | 表单/模式切换下方 | 切到「忘记密码」模式(原「请联系管理员」纯文案已升级为可点击入口) |
| 输入框「邮箱或用户名」 | 表单(忘记密码模式) | 提交自助找回申请,任一即可(后端 identifier,亦兼容 email/username 字段) |
| 按钮「发送重置邮件」 | 表单(忘记密码模式) | 调 POST /auth/forgot-password;loading 时文案变「发送中...」 |
| 输入框「重置令牌」 | 表单(重置密码模式) | 粘贴邮件中的令牌;从 ?reset_token= 进入时自动预填 |
| 输入框「新密码」/「确认新密码」 | 表单(重置密码模式) | 前端先校验「≥8 位」「两次一致」,不通过直接提示且不发请求 |
| 按钮「重置密码」 | 表单(重置密码模式) | 调 POST /auth/reset-password;成功后清空令牌与密码并切回登录模式 |
| 链接「去重置密码」/「返回登录」 | 表单底部(找回模式) | 无邮件直达链接时手工进入重置表单 / 返回登录 |
次要控件:表单上方 alert 提示条展示成功/失败消息,错误文案如实透传后端 error 字段(如「发送失败:请求过于频繁,请稍后再试」「重置失败:重置链接无效或已过期,请重新申请」)。
5. 后端关联
5.1 API 客户端
登录/注册用 action/web/src/api/client.js(axios baseURL /api/v1,timeout 30s,请求拦截器附 Bearer aip_token,401 清 token 跳 /login);自助找回用 action/web/src/api/aipClient.js(baseURL /aip-api/v1,同一套拦截器语义)。
5.2 端点表
| 方法 | 路径 | 请求体 | 页面触发点 | 成功响应 |
|---|---|---|---|---|
| POST | /auth/login | {username, password} | 登录模式提交 | {user_id, username, token, token_type:"bearer"} |
| POST | /auth/register | {username, password, email?} | 注册模式提交 | 201 {user_id, username} |
| POST | /auth/forgot-password | {identifier}(兼容 email/username) | 忘记密码模式提交 | 200 {success:true, message:"如果该账号存在且已绑定邮箱,我们已发送密码重置邮件…"} |
| POST | /auth/reset-password | {token, new_password} | 重置密码模式提交 | 200 {success:true, message:"密码重置成功,请使用新密码登录。"} |
注:/auth/forgot-password 与 /auth/reset-password 注册在 AIP 公开路由组(products/aip/server/server.go,与 /auth/login、/auth/register 同级,无鉴权中间件),前端经 /aip-api 前缀恒定打到 AIP 18080。管理员侧的 POST /api/v1/users/:id/password(admin 组)仍保留,用于「用户管理」页(UsersPage.vue)人工重置。
5.3 关键机制
doLogin 兼容两种后端响应——AIP(18080) 返回顶层 {token, username},Foundry(18081) 返回 {code:0, data:{...}} 信封,前端先判断 data.data.token 是否存在再取内层,随后落库 aip_token/aip_username;Gotham 后端返回 refresh_token 时同步存入 gotham_refresh_token(供 401 自动重放)。登录成功后读 route.query.redirect 决定落地页(默认 /foundry);注册成功后自动切回登录模式。忘记密码成功后停留在原表单并展示后端统一报文;重置成功后清空令牌与密码、切回登录模式并(若 URL 带 reset_token)用 router.replace 清掉 query,避免令牌残留在浏览器历史里。
6. 权限与安全
- 登录/注册/忘记密码/重置密码四个接口均为公开路由(
api组,无鉴权中间件)——找回流程必须在未登录态可用,故以「令牌本身即凭证」承担鉴权。 - 不可枚举:忘记密码对「账号存在 / 不存在 / 未绑定邮箱」返回逐字节相同的 200 报文;发信在服务端异步执行,两条分支的响应耗时一致,不构成时序侧信道。
- 令牌即凭证:令牌为 256-bit 随机值(
crypto/rand),库中仅存 SHA-256 哈希,单次使用(原子置位used_at),默认 30 分钟有效;重置成功即作废。 - 限流:同一账号标识与同一 IP 双维度滑动窗口限流,超限返回 429「请求过于频繁,请稍后再试」;账号维度对存在与不存在的标识一视同仁,429 不泄露账号是否存在。
- 邮件未配置:后端明确返回 503「邮件服务未配置,无法发送重置邮件,请联系管理员」,前端如实展示,不会出现「提示已发送但实际没发」。
- 账号密码校验由后端 authService 完成(登录失败返回 401/403 语义错误,前端提示「操作失败:<error>」)。
- Token 存浏览器 localStorage,后续请求由各客户端拦截器统一以
Authorization: Bearer <aip_token>附带。
7. 常见问题与排错
- 登录成功却立刻跳回登录页:原因是后端 401(token 无效),client.js 拦截器清除 token 并重定向。处理:重新登录;检查服务器时钟与 SECRET_KEY 是否稳定;频繁 401 看后端日志。
- 提示「操作失败:invalid request body」:原因是请求体字段缺失或格式不符(如密码为空、Content-Type 错误)。处理:确认用户名密码均已填写;用 curl 直接调
POST /api/v1/auth/login复现。 - 注册成功但无法登录:原因是注册接口只创建账号不返回 token,需切登录模式再登录。处理:按提示「请切换到登录模式登录」切换;若仍失败检查用户名是否已存在。
- 访问 /login 被直接跳到 Foundry:原因是 localStorage 已有
aip_token,guest 页对已登录用户重定向。处理:属预期行为;如需换账号先在顶部导航栏登出(清 token)。 - 点了「发送重置邮件」但收不到信:先看提示条文案。若为 503「邮件服务未配置」,说明服务端未配 SMTP,需联系管理员;若为 429 则短时间内申请过多,等待窗口过期再试;200 但没收到则依次检查收件箱/垃圾邮件、账号是否绑定了该邮箱、注册时邮箱是否填错(邮箱选填,未填则不发信)。
- 提示「重置链接无效或已过期,请重新申请」:令牌已使用、已过期(默认 30 分钟)或已被更新的一次申请作废。处理:回到「忘记密码」重新申请;每次新申请都会让旧令牌立即失效。
- 重置时提示「新密码长度至少 8 位」:前端与后端同口径校验(后端返回 422
VALIDATION_ERROR)。处理:改用至少 8 位的新密码;弱密码失败不会消耗令牌,改正后可继续用同一令牌提交。
8. 已知缺陷与边界
- 前端对后端响应做「双信封兼容」判断(
data.data.token),后端结构再变化时需同步调整。 - 本页为全产品共用登录页:登录后默认落地 Foundry;Apollo 有独立登录页
/apollo/login(apollo_token),不走本页。 - 自助找回当前只作用于 AIP 账号库:端点注册在 AIP(用户表在
action/data/platform.db),Foundry/Gotham/Swift 若使用各自独立的用户库,其账号在本页申请找回时会被当作「账号不存在」而统一返回 200(不发信、不报错)。跨产品统一找回需各产品各自装配platform/auth.PasswordResetService并注册同名路由。 - 重置成功不吊销无状态 access token:平台 JWT claims 为
sub/id/roles/exp,无jti与令牌版本号,服务端无法在不改全局ParseToken语义的前提下作废已签发的 access token(默认有效期由security.access_token_expire_minutes决定)。重置密码会尽力吊销该用户的 refresh 会话(auth_sessions表存在时),已有 access token 需等自然过期;生产环境建议把 access token 有效期调短。 - 邮箱为选填:注册时未填邮箱的账号无法自助找回,仍需管理员在「用户管理」页重置(
POST /api/v1/users/:id/password)。 - 限流为单进程内存实现,多实例部署下各实例独立计数,需替换为共享存储(Redis 等)才能做到全局限额。