1. 页面概览
1.1 是什么
「登录」页面(对应前端源码 action/web/src/views/apollo/LoginPage.vue)是 LightApollo 控制面的独立账号登录入口。LightApollo 交付运维平台使用自己的一套账号体系,与同仓库其它产品刻意隔离:页面上的提示语明确写着「与 AIP 用户库分离,请使用 Apollo 平台账号登录(默认管理员 admin / admin1)」,Vue 源码顶部注释也记录了两条铁律——其一「Apollo 控制面与 AIP 用户库分离(不同 DB / 不同 user UUID),必须使用 Apollo 自身账号登录」,其二「aip_token 无法访问 Apollo 受保护接口(实测 40301)」。换句话说,直接拿 AIP 的登录态来逛 Apollo 行不通,必须先在本页用 Apollo 账号换取专属会话令牌 apollo_token。
要理解本页在整个前端体系里的位置,需要同时看清三条登录线索的并列关系:全局 /login 是 AIP/Foundry 等产品的登录页(持有 aip_token);/apollo/login 是 Apollo 独立登录页(持有 apollo_token);Swift 产品则走页面内嵌登录(维护 swift_token)。三者彼此独立、令牌语义互不相同,这正是「Apollo 账号体系隔离」在工程上的落点。本页存在的必要性就源于此:Apollo 后端路由把非登录接口全部挂在受保护组下,若没有一张独立登录页,用户将没有任何前端手段获得 Apollo 自己的会话。
页面注册在整站路由的顶层 /apollo/login,不在 ApolloLayout 侧边栏布局之内,也没有 requiresAuth 标记。这是设计上的必然:路由守卫会拦截一切要求认证的页面,登录页若是受保护路由就永远无法到达,因此它被刻意做成少数"未登录也能访问"的页面之一。页面形态是一张居中卡片:品牌名、副标题、一句账号体系提示、可消失的结果提示条,以及「用户名 + 密码 + 登录」的标准表单;卡片底部保留「返回部署总览」的文字链接。
一次完整的登录闭环是:填写 Apollo 账号密码 → 点击「登录」→ 前端把表单以 JSON 提交到后端 POST /auth/login → 后端 bcrypt 校验密码并签发 JWT → 前端把令牌写入 localStorage(键 apollo_token)、把用户名另存为 apollo_username → 提示「登录成功!」→ 前端路由跳转 /apollo 部署总览 → 总览页随即用该令牌调用受保护接口拉取期望状态列表。也就是说,本页输出的是一份持久的会话凭据,它是后续所有 Apollo 页面读写接口的通行证;凭据一旦失效(过期、被清、被后端拒绝),接口层的响应拦截器会把用户自动送回本页重新登录,形成「登录 → 使用 → 失效 → 再登录」的完整闭环。
1.2 核心价值/能力表
| 能力 | 说明 | 对应页面操作 |
|---|---|---|
| Apollo 独立账号登录 | 用 Apollo 自身账号(默认 admin/admin1)换取 apollo_token,与其它产品的登录态隔离 | 输入用户名、密码后点「登录」 |
| 会话令牌持久化 | 登录成功把 apollo_token 写入 localStorage;无 apollo_token 时读取器回退 aip_token 兜底 | 前端自动执行,无需干预 |
| 失败即时反馈 | 凭证错误、账号锁定、网络异常都会在卡片内提示「登录失败:…」,不整页刷新 | 结果提示条自动展示 |
| 登录态死循环防护 | 响应拦截器在 401 时豁免登录页自身,避免「清 token → 跳登录页 → 登录页又 401」的刷新重载死循环 | 页面自动处理 |
| 返回入口 | 卡片底部提供回部署总览的文字链接,供误入本页时返回 | 点「返回部署总览」 |
| 双登录态兼容 | getApolloToken 无 apollo_token 时回退 aip_token,兼容"先登 AIP 再进 Apollo"旧流程 | 由客户端自动完成 |
1.3 一句话总结
登录页是 LightApollo 独立账号体系在 Web 端的唯一入口,职责是把 Apollo 账号密码兑换成可访问受保护接口的 apollo_token 会话,并跳转到部署总览开启交付运维会话。
2. 访问入口
2.1 路由与菜单
| 项 | 值 |
|---|---|
| 路由 path | /apollo/login |
| 路由 name | ApolloLogin |
| meta.title | Apollo 登录(未设置 requiresAuth,也未设置 guest) |
| 布局 | 无产品布局(顶层路由,不在 ApolloLayout children 内,注册于 /apollo 父路由之前) |
| 前端源码 | action/web/src/views/apollo/LoginPage.vue(按 ApolloLoginPage 名导入) |
| API 客户端 | action/web/src/api/apolloClient.js |
| 路由注册 | action/web/src/router/index.js 约 397-402 行(独立顶层路由块) |
| 路由守卫 | 同文件约 805-828 行 beforeEach |
本页不出现在 ApolloLayout 侧边栏的普通菜单里;ApolloLayout 侧边栏底部(分隔线之后)提供「登录 Apollo」链接指向 /apollo/login,通常是侧边栏最后一项,供需要重新登录或切换账号时使用。由于登录成功默认落地 /apollo,日常从侧边栏进入产品后很少再回到本页。
2.2 认证与权限
页面访问无需认证:meta 不含 requiresAuth,守卫遍历 to.matched 找不到该标记即放行,任何人在未登录状态下都能直接打开 /apollo/login。
路由守卫的令牌策略(针对 /apollo 前缀路由):守卫先判断目标路径是否以 /apollo 开头,是则读 localStorage['apollo_token'],读不到再回退读 localStorage['aip_token'](向后兼容「先登 AIP、再进 Apollo」的双登录旧流程);其余产品路由只校验 aip_token。若页面要求认证而两类令牌皆无,守卫重定向到 {name:'Login'}(即全局 /login 页、AIP 产品登录页)并带 redirect 参数记录来路。守卫内还有两条旁路规则:路径为 /(空落地页)时按 localStorage['zy_landing'] 跳转、无记录则默认进 /foundry;meta 带 guest 且已登录时把用户送去已登录落地页。由此产生几个容易混淆的行为,特此点明:
- 不带任何令牌直接访问
/apollo(部署总览等受保护页)→ 先被引到 AIP 的/login,完成 AIP 登录后持有 aip_token 即可经守卫放行(这走的是"双登录旧流程"路径); - 直接访问
/apollo/login→ 永远直达 Apollo 独立登录卡,与 AIP 登录态无关; - 想让权限解析与守卫判定完全一致(推荐做法),就在 Apollo 登录页用 Apollo 账号登录,获得独立的 apollo_token;
- 本页 meta 不含 guest 标记,因此即使已经持有一堆令牌,手动访问
/apollo/login依然能看到登录卡,不会像 guest 页那样被直接送走——这保证了用户随时能回到登录卡重新换令牌。
后端接口鉴权:/auth/login 属公开端点,无需任何令牌即可调用;除此之外,Apollo 全部业务接口都在 protected 路由组内,要求请求头携带 Authorization: Bearer {token}。认证失败(无令牌/令牌无效)后端返回 40101,认证通过但权限不足返回 40301——具体形态见 5.3 的错误 envelope 示例。
2.3 端口与 API 前缀
- Apollo 后端端口:18082。
- 前端请求路径:登录页用 apolloClient 发
POST /auth/login,实际 URL 为/apollo-api/v1/auth/login;Vite 开发服务器把/apollo-api代理到http://127.0.0.1:18082,并做前缀重写/apollo-api → /api,因此后端收到的是/api/v1/auth/login(Apollo 路由统一注册在/api/v1下,与 agent 的 pull/report 等公开端点在同一个 API group)。 - 生产形态注意:Vite 代理只存在于开发服务器;构建后的静态页由站点网关按
/apollo-api前缀分流到 18082(多产品分端口代理的既有约定),前缀重写规则须与网关配置保持一致,否则登录请求会 404。 - apolloClient 配置:baseURL
/apollo-api/v1、timeout 30000 ms、默认请求头Content-Type: application/json。
2.4 到达本页的常见途径
登录卡看似冷门,实际有四种典型到达路径,理解它们有助于排错时判断"为什么用户停在登录页":
- 主动重新登录/切换账号:从 ApolloLayout 侧边栏底部「登录 Apollo」进入——这是设计内最常见的路径,目标明确是"换一个 Apollo 账号或刷新会话";
- 会话失效被自动送回:登录态过期或被清后,任意 Apollo 受保护接口返回 401,apolloClient 响应拦截器执行
window.location.href='/apollo/login'硬跳转——这是"被迫重新登录"的主路径; - 直接输入网址/书签:
/apollo/login是公开路由,任何状态都能直接打开,适合在没有会话的浏览器里提前完成登录; - 守卫引导的间接结果:没有任何令牌时访问
/apollo会被先引到全局/login(AIP 登录页),在 AIP 登录后若接口层仍因跨产品权限被拒(40301),最终仍会落到本页补登 Apollo 账号。
其中路径 2 与 4 的差异值得反复强调:2 是"有 Apollo 会话但失效",4 是"根本没有 Apollo 会话、只有 AIP 会话"。前者的正确动作是重登 Apollo;后者的根治动作同样是到本页拿到 apollo_token,仅靠 AIP 登录并不能保证 Apollo 接口放行。
3. 界面布局
┌────────────────────────────────────────────────┐
│ .apollo-login-page(flex 全屏居中,min-height) │
│ ┌── .apollo-login-card(宽 400px 白底圆角卡)──┐ │
│ │ LightApollo (h1 蓝色标题) │ │
│ │ 交付运维平台 · 独立登录 (灰副标题) │ │
│ │ 与 AIP 用户库分离,请使用 Apollo 平台账号 │ │
│ │ 登录(默认管理员 admin / admin1)(灰提示) │ │
│ │ [ 结果提示条 .alert ](v-if="message") │ │
│ │ 用户名 [ apollo-username 输入框 ] │ │
│ │ 密码 [ apollo-password 输入框 ] │ │
│ │ [ 登录 / 登录中... ](整宽主按钮,提交禁用) │ │
│ │ ──────────────────────────────── │ │
│ │ 返回部署总览 (.apollo-login-footer)│ │
│ └─────────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
各板块职责:
| 板块 | 职责 |
|---|---|
| 标题区 | 「LightApollo」品牌名 + 「交付运维平台 · 独立登录」副标题,交代产品归属与登录性质 |
| 账号提示 | 声明 Apollo 与 AIP 用户库分离、须用 Apollo 平台账号,并给出默认管理员 admin/admin1 |
| 结果提示条 | 承载登录过程与结果消息(初始 alert-info,成功转 alert-success,失败转 alert-error),有消息才渲染 |
| 表单区 | 用户名/密码两个必填输入框 + 主登录按钮,@submit.prevent 触发 handleSubmit |
| 页脚链接 | 「返回部署总览」router-link,点击跳 /apollo(name: Apollo) |
视觉上卡片宽度 400px、内边距 40px、圆角 12px、细边框加浅阴影,整体为极简的单卡布局。页面不渲染任何侧边栏、顶栏或其它产品的导航元素,也不带脚手架的壳层样式——它是整站最"干净"的页面之一,职责单一到只有"换令牌"一件事。值得补充的是布局细节与意图:整卡以 flex 在视口内垂直水平居中,min-height: calc(100vh - 120px) 保证小屏也居中且不贴边;产品名使用品牌蓝(#1a73e8)加粗,副标题与提示用灰色降低视觉权重,让主表单成为唯一的视觉焦点;表单控件宽 100% 拉伸(.btn-block),登录按钮与输入框等宽,符合"单主操作"的界面直觉——整页只有一个真正的行动点,不存在操作分叉。
4. 交互元素
本章按"能点/能填的每个控件 + 每条行为规则"逐项拆解:4.1 与 4.2 是两个输入框,4.3 是主登录按钮,4.4 是底部链接,4.5 是结果提示条,4.6 给出从点击到跳转的十步行为链,4.7 汇总失败文案来源,4.8 讲键盘与可用性。全页控件总数少,但每条规则(disabled 时机、文案切换、令牌写入、跳转目标)都直接影响登录成败,值得逐项读细。
4.1 用户名输入框
| 属性 | 值 |
|---|---|
| 控件名 | 用户名(label 文案「用户名」,id=apollo-username) |
| 位置 | 表单第一行 |
| 类型/占位 | type=text;placeholder「请输入用户名」 |
| 必填 | 是(HTML required,浏览器层拦截空提交) |
| 自动填充 | autocomplete=username |
| 绑定值 | v-model="form.username" |
| 操作效果 | 键入的内容进入登录请求体 {username} |
| 触发后端调用 | 无(仅作为请求体字段) |
| 边界与细节 | required 校验发生在浏览器层;若绕过前端直接 POST 空值,后端返回 40001「username/password 不能为空」 |
4.2 密码输入框
| 属性 | 值 |
|---|---|
| 控件名 | 密码(label 文案「密码」,id=apollo-password) |
| 位置 | 表单第二行 |
| 类型/占位 | type=password(点按掩码显示);placeholder「请输入密码」 |
| 必填 | 是(HTML required) |
| 自动填充 | autocomplete=current-password |
| 绑定值 | v-model="form.password" |
| 操作效果 | 键入内容进入登录请求体 {password};登录后后端只比对 bcrypt 哈希,明文不落库、不入审计 |
| 触发后端调用 | 无(仅作为请求体字段) |
| 边界与细节 | autocomplete=current-password 提示浏览器该输入框是密码语义;本页不提供「显示密码」切换钮 |
4.3 登录按钮
| 属性 | 值 |
|---|---|
| 控件名 | 「登录」(class 为 btn btn-primary btn-block,整宽主按钮) |
| 位置 | 密码框下方 |
| 可用条件 | 空闲恒可用;请求进行中 :disabled="loading" 置灰 |
| 文案切换 | 空闲「登录」;loading=true 时「登录中...」 |
| 操作效果 | 触发表单 @submit.prevent → handleSubmit():置 loading=true、清空旧消息 → apiClient.post('/auth/login', {username, password}) → 成功分支:若响应缺 data.token 则抛 Error('登录响应缺少 token');setApolloToken(data.token) 把令牌写入 localStorage['apollo_token'];若响应含 username 再写入 localStorage['apollo_username'];提示条显示「登录成功!」并置 alert-success;随后 router.replace('/apollo') 落地部署总览。失败分支:提示条显示 登录失败:{err.message || '请求失败'} 并置 alert-error。finally 复位 loading |
| 触发后端调用 | POST /auth/login(唯一接口) |
| 边界与细节 | ① 登录接口返回裸 JSON而非统一 envelope,前端只信任其中的 token 字段,缺 token 一律视为失败;② 错误文案取拦截器改写过的 err.message(即服务端 envelope 的 message,失败时多为英文,如 Authentication failed);③ 成功提示与 router.replace 几乎同帧发生,页面随即跳转,用户通常来不及看到「登录成功!」这一条 |
4.4 返回部署总览链接
| 属性 | 值 |
|---|---|
| 控件名 | 「返回部署总览」(.apollo-login-footer 内 .link-btn) |
| 位置 | 登录卡片底部、表单之外 |
| 操作效果 | router-link 跳 /apollo(路由 name: Apollo,部署总览页) |
| 边界与细节 | 点击后是否真正到达取决于守卫:已持 apollo_token 或可回退的 aip_token 才能通过;两者皆无时会被引到全局 /login |
4.5 登录结果提示条
| 属性 | 值 |
|---|---|
| 控件名 | 结果提示条(v-if="message" 才渲染,class=alert + 动态 messageType) |
| 初始状态 | message 为空串,不渲染;messageType 初始 alert-info |
| 类型切换 | 成功 alert-success(绿);失败 alert-error(红) |
| 展示文案 | 成功「登录成功!」;失败「登录失败:{err.message 或 '请求失败'}」 |
| 生命周期 | 每次提交开头清空,提交结束按结果回填;页面无手动关闭按钮,跳转或再次提交即覆盖 |
4.6 提交与校验行为链
一次点击「登录」从前端到后端的完整行为链(供排错对照):
- 浏览器先做 HTML5 required 校验:用户名或密码为空时表单不触发 submit,按钮无任何网络请求;
- 通过后触发
handleSubmit:loading=true使按钮置灰并显示「登录中...」,message=''清空旧提示; - 前端把
{username, password}JSON 化,经 apolloClient 以POST /apollo-api/v1/auth/login发出(拦截器此时会附带当前已有的 token——公开端点不校验,无碍); - 后端读请求体,若用户名或密码为空返回 40001「username/password 不能为空」;
- 后端按用户名查用户:用户不存在或密码不匹配 → 非 admin 失败计数 +1,满 5 触发锁定与
ACCOUNT_LOCKOUT审计并返回锁定错误,否则返回Authentication failed(401); - 密码正确但账号被锁定且非 admin → 返回
Account locked. Please try again later.;admin 豁免锁定并自动解锁; - 密码正确 → 平台签发访问令牌(payload 含
sub=用户名等声明),记USER_LOGIN成功审计,返回 200 + 裸 JSON{user_id, username, token, token_type}; - 响应回到 apolloClient 响应拦截器:body 无数字
code字段 → 判定非 envelope、原样放行; - 页面取
data.token:缺失则抛「登录响应缺少 token」进失败分支;存在则写apollo_token、写apollo_username、提示「登录成功!」; router.replace('/apollo')跳转总览,finally复位 loading。
其中第 5、6 步属于后端 auth 服务的账号策略,第 8、9 步是本页与客户端解包约定配合的关键衔接。
4.7 失败文案来源映射
页面失败提示统一形如「登录失败:{来源文案}」,不同失败点的文案来源不同,据此可快速定位问题层:
| 失败场景 | 实际呈现 | 来源 |
|---|---|---|
| 用户名/密码为空被浏览器拦截 | 不触发请求,无提示 | 浏览器 HTML required |
| 绕过前端发空值请求 | 「登录失败:username/password 不能为空」 | 后端 40001 |
| 用户名不存在或密码错误 | 「登录失败:Authentication failed」 | 后端哨兵错误 ErrAuthentication(401) |
| 非 admin 账号已锁定 | 「登录失败:Account locked. Please try again later.」 | 后端 AuthorizationError |
| 后端未启动/代理未就绪 | 「登录失败:Network Error」或「登录失败:请求失败」 | axios / 兜底文案 |
| 响应异常缺 token | 「登录失败:登录响应缺少 token」 | 页面自定义 Error |
次要控件与行为说明:本页没有「记住我」「忘记密码」「注册」「第三方登录」等任何附加控件;注册能力虽在后端存在(POST /auth/register,201 返回 {user_id, username}),但未暴露到本登录页,Apollo 账号的创建由后端引导初始化或用户管理能力完成。表单无自定义校验文案,全部依赖 HTML5 required 与后端错误信息。
4.8 键盘与可用性行为
- 回车提交:两个输入框任一内按 Enter 都会触发表单 submit(form 默认行为 +
@submit.prevent),无需点按钮——这是登录表单的标准可用性约定。 - Tab 顺序:用户名 → 密码 → 登录按钮,天然按 DOM 顺序;按钮 focus 后再按 Enter 等同于点击。
- 重复提交防护:请求进行中按钮
:disabled="loading",同时@submit.prevent只挂一次,连点或连按 Enter 在飞行期间不会发出第二个请求;loading 在finally中无条件复位,即使请求异常也会恢复按钮。 - 失败可重试:登录失败不清空已填的用户名(仅密码由浏览器密码管理器决定是否回填),用户改密后可直接再次提交。
- 自动填充语义:两个输入框的
autocomplete分别声明 username 与 current-password,密码管理器据此在同一个会话里正确关联账号密码;由于本页路由与 AIP 登录页不同,两个产品各自的登录表单不会互相污染保存的凭据。
5. 后端关联
5.1 API 客户端
apolloClient.js(action/web/src/api/apolloClient.js)是 Apollo 全部页面共用的 axios 实例,登录页复用它,从而自动获得一致的鉴权与解包行为:
- 实例:
axios.create({ baseURL: '/apollo-api/v1', timeout: 30000, headers: {'Content-Type':'application/json'} });令牌键常量TOKEN_KEY = 'apollo_token'。 - 令牌三函数:
setApolloToken(token)写localStorage['apollo_token'];getApolloToken()先读 apollo_token、无则回退localStorage['aip_token'];clearApolloToken()仅删除 apollo_token。登出/会话清理没有独立页面,本质就是让 apollo_token 不存在或失效。 - 请求拦截器:对每个请求附加
Authorization: Bearer {getApolloToken()},无令牌则不加头。 - 响应拦截器(登录页行为的关键):
- 先放行非 JSON 形态:
responseType为 blob/arraybuffer 的下载响应原样返回;响应体无数字code字段的(登录裸 JSON、/agent/pull等公开裸响应)也原样返回; - 命中统一 envelope(body.code 为数字)时,
code===0解包——把response.data直接替换为业务数据;HTTP 2xx 但code!==0视为业务失败 reject; - HTTP 4xx/5xx 错误分支:从
error.response.data.message || error提取文案回填error.message(并别名写入error.response.data.error,兼容旧页面取值);401 时clearApolloToken(),且仅当window.location.pathname !== '/apollo/login'才window.location.href='/apollo/login'硬跳转——登录页自身 401(密码错)就在豁免清单内,避免「清 token → 跳登录页 → 登录页又发 401 → 又跳」的刷新重载死循环;403 时alert('无权限执行该操作')。
登录页正是在上述第 3 点豁免下正常工作的:凭证错误产生 401 时,拦截器只清 token 不跳转,页面 catch 分支把服务端 message 展示为「登录失败:…」。
从调用方视角再对比一次"解包前后"的差异,能解释为何登录页代码里写的是 data.token 而不是其它页面的 data.data.token:普通业务接口的响应带数字 code,拦截器在 code===0 时已把 response.data 替换为业务数据本体,页面 const { data } = await apiClient.get(...) 直接拿业务数据;而登录接口的裸 JSON 没有 code 字段,拦截器原样放行,页面拿到的 data 就是整个响应体 {user_id, username, token, token_type}。两种形态被同一套拦截器兼容,这是阅读其它 Apollo 页面代码时也会反复遇到的模式。
5.2 端点表
| 方法 | 路径(后端) | 请求体 | 页面触发点 | 鉴权 |
|---|---|---|---|---|
| POST | /api/v1/auth/login | {"username":"…","password":"…"} | 点「登录」 | 公开(无令牌要求) |
| POST | /api/v1/auth/register | {"username":"…","password":"…"} | 页面未接入(后端能力) | 公开 |
后端登录 handler(handlers.go handleLogin)执行顺序:读请求体 → 空用户名或空密码直接返回 40001「username/password 不能为空」→ 调平台 auth 服务 Login(先按用户名查库;用户不存在或 bcrypt 不通过返回哨兵错误 ErrAuthentication,HTTP 401、message 为 Authentication failed;非 admin 失败计数满 5 触发锁定并审计 ACCOUNT_LOCKOUT,admin 豁免)→ 校验锁定态与 MFA → 通过后签发访问令牌 → 返回 200 + 裸 JSON。登录成功/失败均写平台审计事件 USER_LOGIN(status=success/failed)。相关审计事件一览:
| 审计事件 | 触发点 | 说明 |
|---|---|---|
| USER_LOGIN | 每次登录尝试 | 携带 username 与 status=success/failed;失败原因如 account locked / mfa_code_required |
| ACCOUNT_LOCKOUT | 非 admin 连续失败满 5 次 | reason=too many failed login attempts |
同一鉴权体系内还有几个与登录页相邻、可用于验证"令牌是否真的生效"的受保护能力:后端 handleMe 类接口返回当前主体的 user_id、username、roles、is_super 与 permissions(绝不返回密码哈希等敏感字段)。拿到新令牌后直接调这类接口即可快速确认登录成功且权限解析正确——比进页面看列表更直接,也更适合写脚本做联调自检。
5.3 响应结构示例
登录成功(HTTP 200,裸 JSON,不套 envelope)——这也是 TAD-12 信封规则的特例,code 字段缺失即原样放行:
{
"user_id": "c1e2f3a4-…",
"username": "admin",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInVpZCI6ImMxZTJmM2E0…",
"token_type": "bearer"
}
| 字段 | 含义 |
|---|---|
| user_id | 用户在 Apollo 独立用户库中的标识(与 AIP 的 user UUID 不同库、不同值) |
| username | 登录用户名;前端会把它另存为 localStorage['apollo_username'] |
| token | 会话 JWT(payload 含 sub=用户名等声明),前端存 localStorage['apollo_token'] 并在后续请求经 Bearer 头携带 |
| token_type | 固定 bearer |
登录失败(HTTP 4xx/5xx,统一错误 envelope)——凭证错误示例(错误文案以服务端返回为准,此处为源码中哨兵错误的真实 message):
{
"code": 40101,
"message": "Authentication failed",
"data": null,
"request_id": "req_8f3a9d…"
}
字段说明:code 为 TAD-12 统一业务码(常见:40001 参数错误、40101 认证失败、40301 权限不足、40401 资源不存在、40901 冲突、42201 校验失败、42901 频率超限、50001 内部错误);message 服务端描述,登录页显示为「登录失败:{message}」;data 失败时恒 null;request_id 为链路追踪号,排查时可与后端日志对查。另有一个值得注意的多形态点:成功是裸 JSON、失败是 envelope,两种形态由 apolloClient"是否有数字 code"的判定自动兼容,页面无需分叉处理——这也是 5.1 拦截器设计的原因。
5.4 关键机制
令牌生命周期与多产品关系。Apollo 登录签发的 JWT 由平台共享 SECRET_KEY 签名(与 AIP 同一套签名密钥、同一 Token 解析器),因此令牌在结构上任意产品都能解析;但 Apollo 的授权解析在自身独立的 user_roles/角色-权限体系内完成,与本页「用户库分离」的说法一致。令牌的一生:登录签发 → 前端持久化到 apollo_token → 每次请求经请求拦截器附加 → 任一受保护接口返回 401(过期/被清/被吊销)时响应拦截器统一清 token 并送回 /apollo/login(已在登录页则豁免)。登出没有按钮,做法是手动清掉 apollo_token(刷新后守卫即不再放行);clearApolloToken 只删 apollo_token、不动 aip_token,语义上是两套独立登录态分别开关。
双登录态与权限兜底。历史 issue 与 changelog 2026-08-11-2-apollo-cross-product-token-403.md 记录了"跨产品 token 访问 Apollo 返回 403"的根因与修复:JWT 可解析 ≠ 可授权,Apollo 受保护接口要求调用方在其自身用户库有对应角色。若调用方来自 AIP(user_id 在 Apollo 查不到角色),Apollo 会按 token 的 username 反查本库同名用户再取角色兜底——两端都存在同名用户(例如都是 admin/admin1)时等效放行,否则 40301。页面提示与 getApolloToken 的 aip_token 回退,正是围绕这套双库/兜底语义设计的;最干净的使用方式是始终在 /apollo/login 用 Apollo 账号登录。
令牌解析的前置条件。令牌签发与校验依赖共享 SECRET_KEY 配置:后端令牌解析器在 SECRET_KEY 缺失或过弱时会回退默认值并发出告警,若密钥不稳定(部署间不一致、被重置),会出现"登录成功即 401""A 节点签发、B 节点不认"的诡异现象。多实例部署时务必让所有节点读到同一份密钥;改密后所有存量 apollo_token 会整体失效,用户需重新登录——这属于可预期的运维事件而非页面缺陷。
账号安全策略。平台 auth 的 Login 承担:密码 bcrypt 比对(明文不落库、审计脱敏);非 admin 连续失败 ≥5 次(maxLoginAttempts=5)后账号锁定并审计 ACCOUNT_LOCKOUT,锁定期间正确密码也拒绝(提示 Account locked. Please try again later.),admin 角色豁免锁定、失败计数不累加,且 admin 用正确密码可自动解锁;登录成功/失败事件 USER_LOGIN 均写审计。若 Apollo 侧为账号接入 MFA(平台 auth 预留了 TOTP 校验位),则缺 TOTP 码会返回 MFA_REQUIRED(message MFA verification required),而本页表单没有 TOTP 输入位——是否命中取决于后端是否给 Apollo 装配 MFAStore,属代码逻辑推断,未实测。
审计与脱敏的衔接。Apollo 服务端对每个 HTTP 请求跑 httpAudit 中间件(记 user_id/username/IP/UA/method/path/query/body/status/action/resource/result/error/duration_ms,并生成 request_id 贯穿响应),落库前经 RedactBody/RedactQuery 脱敏——密码、token、密钥等值替换为 ***、超长(>2KB)截断。登录请求中的 password 字段因此在审计表里只留下脱敏占位;前端排错时把响应里的 request_id 与后端日志/审计对查即可定位单次登录的成败与耗时。
"先登 AIP 再逛 Apollo"到底发生了什么(场景复盘)。很多用户会先被全局落地页 / 引导进 Foundry(默认 zy_landing 未设置时 /foundry),此时持有的是 aip_token。接着用户手动改地址访问 /apollo:① 路由守卫发现目标以 /apollo 开头,先读 apollo_token——没有;② 回退读 aip_token——存在,于是放行;③ 总览页 GET /desired-states 带上的正是 aip_token;④ Apollo 后端解析令牌后按 user_id 在自身角色表查不到 → 走 username 兜底反查;⑤ 两端都是同名管理员则等效放行,名字对不上或角色缺权限则 40301,前端 alert「无权限执行该操作」;⑥ 此时用户到 /apollo/login 用 Apollo 账号登录换 apollo_token,后续请求全部带上新令牌,第 ⑤ 步问题消失。整条链路解释了本页在"混合登录环境"里的不可替代性——它是把会话从"别的产品令牌"切换到"Apollo 自家令牌"的唯一前端开关。
6. 权限与安全
- 凭证保护:密码仅出现在登录请求体并经 HTTPS 传输;后端只存 bcrypt 哈希,永不明文落库;审计链路对密码字段脱敏为
***。 - 失败锁定与告警:非 admin 连续失败满 5 次锁定账号并记审计
ACCOUNT_LOCKOUT,锁定后即使密码正确也拒绝;admin 豁免计数与锁定,正确密码可自动解锁(源码行为)。 - 认证分层:
/auth/login公开;其余 Apollo 接口全部走 Bearer 鉴权,认证失败 40101、授权不足 40301 双通道返回;403 时前端以浏览器 alert「无权限执行该操作」提示,不产生任何写操作、不清会话。 - 令牌的声明与签名:JWT 由平台共享 SECRET_KEY 签名(SECRET_KEY 过弱或漂移会导致配置归零、令牌立即过期),payload 以
sub承载用户名;Apollo 侧授权以自身角色体系解析,杜绝"别的产品签的令牌在 Apollo 默认放行"。 - 默认口令风险:页面明示默认管理员 admin/admin1,属演示期便利设计;正式部署应尽快改密并收敛默认账号。
- 令牌暴露面:apollo_token 存浏览器 localStorage,XSS 可读取;产品内无单独的密钥轮换/吊销页面,令牌生命周期依赖 401 拦截与手动清理。
- 无网络层限速/验证码:登录接口只依赖后端账号锁定策略防爆破,页面本身不提供验证码或节流;对暴露在公网的部署,建议在网关层补充频控(如 42901 语义)。
- 锁定语义的边界:锁定以"非 admin 失败计数 ≥5"触发,admin 永不因失败被锁——这避免管理员被锁死的运营事故,但也意味着 admin 口令是本产品安全边界的重中之重。
7. 常见问题与排错
本章 10 条按"现象 → 原因 → 处理"三句结构给出,覆盖账号隔离、错误文案、锁定策略、会话失效、代理故障、权限不足、守卫行为、双令牌兼容、注册缺口与自检方法。凡标注"由代码逻辑推断"的条目未经实测,其余均有源码或实测依据,排查时可优先对照 4.7 的文案来源映射定位问题层。
- 用 AIP 平台账号在本页登录失败。现象:输入 AIP 的账号密码点「登录」,提示「登录失败:…」。原因:Apollo 与 AIP 用户库分离(不同 DB / 不同 user UUID),本页只认 Apollo 用户库账号。处理:改用 Apollo 账号(默认 admin/admin1,或由用户管理/后端 register 创建的 Apollo 账号)。
- 提示「登录失败:Authentication failed」。现象:密码错误或账号不存在时页面英文报错。原因:后端密码比对失败返回哨兵错误(message 固定
Authentication failed),apolloClient 把它拼进失败提示;该文案来自后端、前端未本地化。处理:核对用户名密码后重试;该提示属预期错误文案,不是前端缺陷。 - 连续多次失败后提示锁定类错误。现象:提示如「登录失败:Account locked. Please try again later.」。原因:非 admin 账号连续失败满 5 次被锁定(锁定事件
ACCOUNT_LOCKOUT可审计)。处理:等待管理员解锁或换正确凭证重试(admin 豁免);仍异常则查后端 auth 日志与用户锁定状态。 - 登录成功却立刻回到登录页/总览打不开。现象:提示过「登录成功!」但页面跳转后接口仍 401 并被送回。原因:apollo_token 未持久化(localStorage 被禁/被清)或令牌签发即过期(SECRET_KEY 漂移、服务器时钟偏差)。处理:DevTools 执行
localStorage.getItem('apollo_token')确认令牌存在;频繁 401 时检查共享 SECRET_KEY 配置是否稳定并查看后端日志。此路径由代码逻辑推断,未实测。 - 登录卡在「登录中...」或提示网络类错误。现象:按钮置灰不跳转,提示「登录失败:Network Error」一类。原因:18082 后端未启动或
/apollo-api代理未就绪(Vite 代理目标 127.0.0.1:18082)。处理:确认 Apollo 后端进程在跑、端口 18082 可达,再刷新重试。 - 访问其它 Apollo 页弹「无权限执行该操作」。现象:浏览器级 alert。原因:某接口返回 40301,常见于持有 aip_token 兜底令牌而该用户名在 Apollo 无角色/权限。处理:到
/apollo/login用 Apollo 账号登录换取 apollo_token;仍失败则由管理员在用户/角色管理核对权限点(如 PermDesiredStateRead 等)。 - 「返回部署总览」点过去却进了 AIP 登录页。现象:无任何令牌时点底部链接被引到
/login。原因:/apollo 受保护且 apollo_token、aip_token 皆无,守卫重定向{name:'Login'}并带 redirect。处理:属预期守卫行为;先在本页登录(或先登 AIP 走兜底),再返回。 - 手动删了 apollo_token 刷新后仍能进总览。现象:看似已"登出",/apollo 仍可访问。原因:守卫回退读 aip_token 兜底放行(双登录旧流程);若后端该用户名无权限仍会接口 403。处理:属预期的双令牌兼容行为;需要"彻底登出 Apollo"时确认 apollo_token 已清除且该浏览器不希望再用 AIP 会话兜底访问 Apollo 页。
- 页面没有注册入口,新成员无法自助开户。现象:新同事打开登录卡只有用户名/密码两个输入位。原因:注册仅后端
POST /auth/register支持,前端登录页刻意不暴露注册能力,账号开通走用户管理链路。处理:由具备权限的管理员在后端或用户管理能力中创建 Apollo 账号(后端 register 会做用户名重复 409 校验与 bcrypt 哈希落库),再交给用户登录。 - 如何用命令行自检登录与令牌。现象:页面表现诡异,想确认是前端问题还是后端问题。原因:需要把浏览器行为与后端行为解耦。处理:用 curl 直连复现——先
POST http://127.0.0.1:18082/api/v1/auth/login带 JSON 体观察裸响应与错误码;再用返回的 token 调一个受保护接口(如期望状态列表)观察 200/40101/40301;对比响应里的 request_id 与后端日志即可判定问题在鉴权层、权限层还是前端代理层。此方法同时适用于复现"用 AIP 令牌访问 Apollo"的 403 场景。
8. 已知缺陷与边界
本章如实列出登录页的功能边界与已知缺口,帮助读者区分"设计如此"与"值得改进"两类问题。其中"注册入口缺失、成功提示不可见、跳转目标硬编码、错误文案未本地化"等属于 web/src 源码层可改进点,已记入临时缺陷报告供主流程收口评估。
| 缺陷/边界 | 说明 |
|---|---|
| 页面能力极简 | 无注册、找回密码、记住我、显示密码、第三方登录入口;注册仅后端 API,MFA 用户可能缺 TOTP 输入位(条件性) |
| 无登出按钮 | 会话清理靠手动删 localStorage 的 apollo_token(或等 401 拦截自动清理),页内无显式入口 |
| 成功提示几乎不可见 | 「登录成功!」与 router.replace('/apollo') 同帧触发,用户通常看不到该条消息 |
| 跳转目标硬编码 | 登录成功固定跳 /apollo,不读取守卫用到的 redirect 参数,从深链登录后不回原页 |
| 双令牌语义并存 | clearApolloToken 只清 apollo_token,aip_token 仍有效;两套会话开关需使用者理解 |
| 错误文案未本地化 | 后端失败 message 为英文(如 Authentication failed),页面原样展示,中文可读性一般 |
| 默认账号明示 | 页面展示 admin/admin1 默认口令,便利演示但弱化安全边界 |
| 成功判定以 token 存在为准 | 前端不额外回验令牌(如调 /users/me),过期令牌要等首个业务接口 401 才暴露 |
| 超时无显式提示 | axios 30s 超时只产生通用网络错误文案,页面不做超时/重试引导 |
| 空值提示依赖浏览器 | 用户名/密码为空的提示文案由浏览器 required 默认样式给出,产品未定制中文校验文案 |
| 界面元素自绘样式 | 登录卡使用组件级 scoped 样式而非全局设计令牌,颜色/间距独立维护,改动不影响其它页面 |
| 无会话时长提示 | 令牌过期时间由后端 JWT 决定,页面不展示"还剩多久失效",失效瞬间以 401 + 跳转呈现 |
| 无多语言 | 页面文案硬编码中文(登录、用户名、密码等),错误文案混入后端英文,无 i18n 抽象 |