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 → 提示「登录成功!」(停留 800ms 供用户看到成功反馈,LoginPage.vue:144-151)→ 前端路由跳转部署总览(?redirect= 且为站内路径时回该路径,否则 /apolloLoginPage.vue:145-152)→ 总览页随即用该令牌调用受保护接口拉取期望状态列表。也就是说,本页输出的是一份持久的会话凭据,它是后续所有 Apollo 页面读写接口的通行证;凭据一旦失效(过期、被清、被后端拒绝),接口层的响应拦截器会把用户自动送回本页重新登录,形成「登录 → 使用 → 失效 → 再登录」的完整闭环。

1.2 核心价值/能力表

能力说明对应页面操作
Apollo 独立账号登录用 Apollo 自身账号(默认 admin/admin1)换取 apollo_token,与其它产品的登录态隔离输入用户名、密码后点「登录」
会话令牌持久化登录成功把 apollo_token 写入 localStorage;读取器仅认 apollo_token,不回退 aip_token(2026-09-12 修订)前端自动执行,无需干预
注册账号(本页内置)已修复(提交 412e9c48):登录卡内置注册模式,调后端公开 POST /auth/register 创建 Apollo 账号卡片底部「注册账号」切换
失败即时反馈凭证错误、账号锁定、MFA、网络异常都会在卡片内提示「登录失败:…」(后端英文消息经 friendlyAuthError 映射为中文,LoginPage.vue:196-206),不整页刷新结果提示条自动展示
登录态死循环防护响应拦截器在 401 时豁免登录页自身,避免「清 token → 跳登录页 → 登录页又 401」的刷新重载死循环页面自动处理
返回入口卡片底部提供回部署总览的文字链接,供误入本页时返回点「返回部署总览」
令牌读取隔离getApolloToken 仅读 apollo_token,不回退 aip_token;Apollo 会话与 AIP 彻底隔离(2026-09-12 修订)由客户端自动完成

1.3 一句话总结

登录页是 LightApollo 独立账号体系在 Web 端的唯一入口,职责是把 Apollo 账号密码兑换成可访问受保护接口的 apollo_token 会话,并跳转到部署总览开启交付运维会话。

2. 访问入口

2.1 路由与菜单

路由 path/apollo/login
路由 nameApolloLogin
meta.titleApollo 登录(未设置 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 前缀路由,2026-09-12 修订):守卫先判断目标路径是否以 /apollo 开头,是则仅认 localStorage['apollo_token'],读不到即重定向到 /apollo/loginApolloLogin),不再回退读 localStorage['aip_token'](Apollo 用户库与 AIP 已分离,跨产品令牌回退会放大越权面);其余产品路由仍校验 aip_token。守卫内还有两条旁路规则:路径为 /(空落地页)时按 localStorage['zy_landing'] 跳转、无记录则默认进 /foundry;meta 带 guest 且已登录时把用户送去已登录落地页。由此产生几个容易混淆的行为,特此点明:

后端接口鉴权/auth/login 属公开端点,无需任何令牌即可调用;除此之外,Apollo 全部业务接口都在 protected 路由组内,要求请求头携带 Authorization: Bearer {token}。认证失败(无令牌/令牌无效)后端返回 40101,认证通过但权限不足返回 40301——具体形态见 5.3 的错误 envelope 示例。

2.3 端口与 API 前缀

2.4 到达本页的常见途径

登录卡看似冷门,实际有四种典型到达路径,理解它们有助于排错时判断"为什么用户停在登录页":

  1. 主动重新登录/切换账号:从 ApolloLayout 侧边栏底部「登录 Apollo」进入——这是设计内最常见的路径,目标明确是"换一个 Apollo 账号或刷新会话";
  2. 会话失效被自动送回:登录态过期或被清后,任意 Apollo 受保护接口返回 401,apolloClient 响应拦截器执行 window.location.href='/apollo/login' 硬跳转——这是"被迫重新登录"的主路径;
  3. 直接输入网址/书签/apollo/login 是公开路由,任何状态都能直接打开,适合在没有会话的浏览器里提前完成登录;
  4. 守卫引导的间接结果:没有任何令牌时访问 /apollo 会被引到本页(/apollo/login);补登 Apollo 账号换取 apollo_token 后才能继续(不再经由 AIP 登录页中转)。

其中路径 2 与 4 的差异值得强调:2 是"有 Apollo 会话但失效",4 是"完全没有 Apollo 会话"(可能只持有别的产品会话)。两者的正确动作一致——到本页用 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")      │ │
│  │  login 模式:                                 │ │
│  │    用户名 [ apollo-username ]                 │ │
│  │    密码   [ apollo-password ]                 │ │
│  │    [ 登录 / 登录中... ](整宽主按钮,提交禁用)   │ │
│  │  register 模式(switchMode 切换):            │ │
│  │    用户名 + 密码 + 邮箱(选填) + [ 注册/注册中... ]│ │
│  │    hint:注册后账号默认无角色,请由管理员分配角色 │ │
│  │  ────────────────────────────────            │ │
│  │  返回部署总览 · 注册账号(login)/              │ │
│  │  已有账号?返回登录(register)(.apollo-login-footer)│ │
│  └─────────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘

各板块职责:

板块职责
标题区「LightApollo」品牌名 + 「交付运维平台 · 独立登录」副标题,交代产品归属与登录性质
账号提示声明 Apollo 与 AIP 用户库分离、须用 Apollo 平台账号,并给出默认管理员 admin/admin1
结果提示条承载登录过程与结果消息(初始 alert-info,成功转 alert-success,失败转 alert-error),有消息才渲染
表单区login 模式:用户名/密码两个必填输入框 + 主登录按钮(@submit.prevent 触发 handleSubmit);register 模式:用户名/密码必填 + 邮箱选填 + 「注册」按钮(触发 handleRegister)
页脚链接login 模式:「返回部署总览」router-link(跳 /apollo,name: Apollo)+「注册账号」切换链接;register 模式:「已有账号?返回登录」切换链接(切换由 switchMode 就地切模式、不跳路由)

视觉上卡片宽度 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.preventhandleSubmit():置 loading=true、清空旧消息 → apiClient.post('/auth/login', {username, password}) → 成功分支:若响应缺 data.token 则抛 Error('登录响应缺少 token')setApolloToken(data.token) 把令牌写入 localStorage['apollo_token'];若响应含 username 再写入 localStorage['apollo_username'];提示条显示「登录成功!」并置 alert-success;停留 800msLoginPage.vue:151)后 router.replace(redirect) 落地(redirectroute.query.redirect 且须为站内路径,否则回落 /apolloLoginPage.vue:145-152)。失败分支:提示条显示 登录失败:{friendlyAuthError(err)} 并置 alert-error(英文后端消息映射为中文,LoginPage.vue:196-206)。finally 复位 loading
触发后端调用POST /auth/login
边界与细节① 登录接口返回裸 JSON而非统一 envelope,前端只信任其中的 token 字段,缺 token 一律视为失败;② 错误文案经 friendlyAuthError 本地化(已修复(提交 412e9c48)too many failed/account locked/mfa verification required/authentication failed 分别映射中文提示,其余原样展示,LoginPage.vue:196-206);③ 已修复(提交 412e9c48):成功提示与跳转之间插入 800ms 停留(:144-151),用户可见「登录成功!」;④ 跳转目标不再硬编码:优先 route.query.redirect(仅接受以 / 开头且非 // 的站内路径),否则 /apollo:145-152

4.4 返回部署总览链接

属性
控件名「返回部署总览」(.apollo-login-footer 内 .link-btn)
位置登录卡片底部、表单之外
操作效果router-link 跳 /apollo(路由 name: Apollo,部署总览页);同一 footer 内另有「注册账号」链接(switchMode('register') 就地切到注册模式,不跳路由,LoginPage.vue:100/161-165
边界与细节点击后是否真正到达取决于守卫:仅持有 apollo_token 才能通过;无 token 时会被引到 /apollo/login

4.5 登录结果提示条

属性
控件名结果提示条(v-if="message" 才渲染,class=alert + 动态 messageType
初始状态message 为空串,不渲染;messageType 初始 alert-info
类型切换成功 alert-success(绿);失败 alert-error(红)
展示文案成功「登录成功!」;注册成功「注册成功,请使用新账号登录」;失败「登录失败:{friendlyAuthError}」(中文映射)或「注册失败:{服务端 message}」
生命周期每次提交开头清空,提交结束按结果回填;页面无手动关闭按钮,跳转或再次提交即覆盖

4.6 提交与校验行为链

一次点击「登录」从前端到后端的完整行为链(供排错对照):

  1. 浏览器先做 HTML5 required 校验:用户名或密码为空时表单不触发 submit,按钮无任何网络请求;
  2. 通过后触发 handleSubmitloading=true 使按钮置灰并显示「登录中...」,message='' 清空旧提示;
  3. 前端把 {username, password} JSON 化,经 apolloClient 以 POST /apollo-api/v1/auth/login 发出(拦截器此时会附带当前已有的 token——公开端点不校验,无碍);
  4. 后端读请求体,若用户名或密码为空返回 40001「username/password 不能为空」;
  5. 后端按用户名查用户:用户不存在或密码不匹配 → 非 admin 失败计数 +1,满 5 触发锁定与 ACCOUNT_LOCKOUT 审计并返回锁定错误,否则返回 Authentication failed(401);
  6. 密码正确但账号被锁定且非 admin → 返回 Account locked. Please try again later.;admin 豁免锁定并自动解锁;
  7. 密码正确 → 平台签发访问令牌(payload 含 sub=用户名等声明),记 USER_LOGIN 成功审计,返回 200 + 裸 JSON {user_id, username, token, token_type}
  8. 响应回到 apolloClient 响应拦截器:body 无数字 code 字段 → 判定非 envelope、原样放行;
  9. 页面取 data.token:缺失则抛「登录响应缺少 token」进失败分支;存在则写 apollo_token、写 apollo_username、提示「登录成功!」;
  10. 停留 800msLoginPage.vue:151)后 router.replace(redirect) 跳转(redirectroute.query.redirect 站内路径或回落 /apollo),finally 复位 loading。

其中第 5、6 步属于后端 auth 服务的账号策略,第 8、9 步是本页与客户端解包约定配合的关键衔接。

4.7 失败文案来源映射

页面失败提示统一形如「登录失败:{来源文案}」,不同失败点的文案来源不同,据此可快速定位问题层:

失败场景实际呈现来源
用户名/密码为空被浏览器拦截不触发请求,无提示浏览器 HTML required
绕过前端发空值请求「登录失败:username/password 不能为空」后端 40001
用户名不存在或密码错误「登录失败:用户名或密码错误」后端哨兵错误 ErrAuthentication(401),前端 friendlyAuthError 本地化(LoginPage.vue:204
非 admin 账号已锁定「登录失败:账号已锁定,请稍后重试」后端 AuthorizationError,前端本地化(LoginPage.vue:201-202
MFA 已开启「登录失败:该账号已开启二次验证,暂不支持在此登录」后端 MFA_REQUIRED,前端本地化(LoginPage.vue:203
后端未启动/代理未就绪「登录失败:Network Error」或「登录失败:请求失败」axios / 兜底文案
响应异常缺 token「登录失败:登录响应缺少 token」页面自定义 Error

次要控件与行为说明:本页没有「记住我」「忘记密码」「显示密码」「第三方登录」等附加控件;已修复(提交 412e9c48):注册能力已在页内暴露——卡片底部「注册账号」切到注册模式(用户名/密码必填 + 邮箱选填),调后端公开 POST /auth/register,成功后回到登录模式并提示「注册成功,请使用新账号登录」(LoginPage.vue:52-94/167-194);注册后账号默认无角色,需管理员在「用户管理」分配角色。表单无自定义校验文案,登录/注册依赖 HTML5 required 与后端错误信息,注册模式另有前端必填提示「用户名与密码不能为空」(:168-172)。

4.8 键盘与可用性行为

5. 后端关联

5.1 API 客户端

apolloClient.js(action/web/src/api/apolloClient.js)是 Apollo 全部页面共用的 axios 实例,登录页复用它,从而自动获得一致的鉴权与解包行为:

  1. 先放行非 JSON 形态:responseType 为 blob/arraybuffer 的下载响应原样返回;响应体无数字 code 字段的(登录裸 JSON、/agent/pull 等公开裸响应)也原样返回;
  2. 命中统一 envelope(body.code 为数字)时,code===0 解包——把 response.data 直接替换为业务数据;HTTP 2xx 但 code!==0 视为业务失败 reject;
  3. 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;由于 Apollo 守卫已不回退 aip_token,清掉 apollo_token 即等于登出 Apollo。

双登录态与权限兜底。历史 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。2026-09-12 修订后前端不再回退 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"(口径变化复盘)。历史版本(2026-09-12 前)中,用户先被全局落地页 / 引导进 Foundry(默认 zy_landing 未设置时 /foundry)持有 aip_token,再手动访问 /apollo:① 路由守卫发现目标以 /apollo 开头,先读 apollo_token——没有;② 回退读 aip_token——存在,于是放行;③ 总览页 GET /desired-states 带上的正是 aip_token;④ Apollo 后端解析令牌后按 user_id 在自身角色表查不到 → 走 username 兜底反查;⑤ 两端同名管理员才等效放行,名字对不上或角色缺权限则 40301,前端 alert「无权限执行该操作」。2026-09-12 修订后守卫仅认 apollo_token:以 /apollo 开头的路由无 apollo_token 直接跳 /apollo/login,AIP 会话不再能"兜底"进入 Apollo;这消除了跨产品令牌回退放大越权面的风险,也让"在 /apollo/login 用 Apollo 账号登录"成为唯一入口。

6. 权限与安全

7. 常见问题与排错

本章 10 条按"现象 → 原因 → 处理"三句结构给出,覆盖账号隔离、错误文案、锁定策略、会话失效、代理故障、权限不足、守卫行为、双令牌兼容、注册缺口与自检方法。凡标注"由代码逻辑推断"的条目未经实测,其余均有源码或实测依据,排查时可优先对照 4.7 的文案来源映射定位问题层。

  1. 用 AIP 平台账号在本页登录失败。现象:输入 AIP 的账号密码点「登录」,提示「登录失败:…」。原因:Apollo 与 AIP 用户库分离(不同 DB / 不同 user UUID),本页只认 Apollo 用户库账号。处理:改用 Apollo 账号(默认 admin/admin1,或由用户管理/后端 register 创建的 Apollo 账号)。
  2. 提示「登录失败:用户名或密码错误」。现象:密码错误或账号不存在时报错。原因:后端密码比对失败返回哨兵错误(message 固定 Authentication failed),已修复(提交 412e9c48) 前端 friendlyAuthError 把它映射为中文「用户名或密码错误」(LoginPage.vue:196-206)。处理:核对用户名密码后重试;账号锁定/MFA 类另有对应中文提示。
  3. 连续多次失败后提示锁定类错误。现象:提示如「登录失败:Account locked. Please try again later.」。原因:非 admin 账号连续失败满 5 次被锁定(锁定事件 ACCOUNT_LOCKOUT 可审计)。处理:等待管理员解锁或换正确凭证重试(admin 豁免);仍异常则查后端 auth 日志与用户锁定状态。
  4. 登录成功却立刻回到登录页/总览打不开。现象:提示过「登录成功!」但页面跳转后接口仍 401 并被送回。原因:apollo_token 未持久化(localStorage 被禁/被清)或令牌签发即过期(SECRET_KEY 漂移、服务器时钟偏差)。处理:DevTools 执行 localStorage.getItem('apollo_token') 确认令牌存在;频繁 401 时检查共享 SECRET_KEY 配置是否稳定并查看后端日志。此路径由代码逻辑推断,未实测。
  5. 登录卡在「登录中...」或提示网络类错误。现象:按钮置灰不跳转,提示「登录失败:Network Error」一类。原因:18082 后端未启动或 /apollo-api 代理未就绪(Vite 代理目标 127.0.0.1:18082)。处理:确认 Apollo 后端进程在跑、端口 18082 可达,再刷新重试。
  6. 访问其它 Apollo 页弹「无权限执行该操作」。现象:浏览器级 alert。原因:某接口返回 40301,通常是当前 apollo_token 对应用户在 Apollo 缺该接口所需的角色/权限点。处理:到 /apollo/login 用具备相应权限的 Apollo 账号登录;仍失败则由管理员在用户/角色管理核对权限点(如 PermDesiredStateRead 等)。
  7. 「返回部署总览」点过去却进了 Apollo 登录页。现象:无任何令牌时点底部链接被引到 /apollo/login。原因:/apollo 受保护且无 apollo_token,守卫重定向 {name:'ApolloLogin'}。处理:属预期守卫行为;先在本页登录换取 apollo_token,再返回。
  8. 手动删了 apollo_token 刷新后会被送回登录页。现象:清掉 apollo_token 后刷新 /apollo 即跳 /apollo/login。原因:守卫对 /apollo 前缀仅校验 apollo_token,2026-09-12 修订后不再回退 aip_token,删掉即无凭证。处理:属预期登出行为;需要继续使用 Apollo 就在本页重新登录换取 apollo_token。
  9. 新成员如何自助开户。现象:新同事需要 Apollo 账号。已修复(提交 412e9c48):登录卡底部「注册账号」进入注册模式(用户名/密码必填 + 邮箱选填),调后端公开 POST /auth/register 创建账号(后端做用户名重复 409 校验与 bcrypt 哈希落库);成功后自动回到登录模式并提示「注册成功,请使用新账号登录」(LoginPage.vue:167-194)。处理:注册后账号默认无角色,需由管理员在「用户管理」分配角色(含所需权限点)后方可正常访问受保护接口。
  10. 如何用命令行自检登录与令牌。现象:页面表现诡异,想确认是前端问题还是后端问题。原因:需要把浏览器行为与后端行为解耦。处理:用 curl 直连复现——先 POST http://127.0.0.1:18082/api/v1/auth/login 带 JSON 体观察裸响应与错误码;再用返回的 token 调一个受保护接口(如期望状态列表)观察 200/40101/40301;对比响应里的 request_id 与后端日志即可判定问题在鉴权层、权限层还是前端代理层。此方法同时适用于复现"用 AIP 令牌访问 Apollo"的 403 场景。

8. 已知缺陷与边界

本章如实列出登录页的功能边界与已知缺口,帮助读者区分"设计如此"与"值得改进"两类问题。其中"注册入口缺失、成功提示不可见、跳转目标硬编码、错误文案未本地化"四项已在提交 412e9c48 修复(见各行 file:line),余下为设计边界。

缺陷/边界说明
页面能力极简已修复(提交 412e9c48):注册入口已内置(详见下条);仍无找回密码、记住我、显示密码、第三方登录入口;MFA 用户可能缺 TOTP 输入位(条件性)
注册入口已修复(提交 412e9c48):页内「注册账号」切换注册模式并调 POST /auth/registerLoginPage.vue:52-94/167-194),成功提示「注册成功,请使用新账号登录」;注册后默认无角色需管理员分配
无登出按钮会话清理靠手动删 localStorage 的 apollo_token(或等 401 拦截自动清理),页内无显式入口
成功提示可见性已修复(提交 412e9c48):跳转前停留 800ms(LoginPage.vue:144-151),用户可见「登录成功!」
跳转目标已修复(提交 412e9c48):优先 route.query.redirect(仅接受以 / 开头且非 // 的站内路径),否则回落 /apolloLoginPage.vue:145-152
登录态隔离clearApolloToken 只清 apollo_token、不影响 aip_token;Apollo 守卫仅认 apollo_token(2026-09-12 修订后不再回退 aip_token)
错误文案本地化已修复(提交 412e9c48)friendlyAuthErrortoo many failed/account locked/mfa verification required/authentication failed 映射为中文,其余原样展示(LoginPage.vue:196-206);未覆盖的英文消息仍可能直接透出
默认账号明示页面展示 admin/admin1 默认口令,便利演示但弱化安全边界
成功判定以 token 存在为准前端不额外回验令牌(如调 /users/me),过期令牌要等首个业务接口 401 才暴露
超时无显式提示axios 30s 超时只产生通用网络错误文案,页面不做超时/重试引导
空值提示依赖浏览器登录表单用户名/密码为空的提示由浏览器 required 默认样式给出;注册模式另有前端提示「用户名与密码不能为空」(LoginPage.vue:168-172
界面元素自绘样式登录卡使用组件级 scoped 样式而非全局设计令牌,颜色/间距独立维护,改动不影响其它页面
无会话时长提示令牌过期时间由后端 JWT 决定,页面不展示"还剩多久失效",失效瞬间以 401 + 跳转呈现
无多语言页面文案硬编码中文(登录、用户名、密码等),无 i18n 抽象;后端未映射的英文错误消息仍可能原样透出