Skip to content

SSO 单点登录与统一登出

account 服务作为 SaaS 产品线的统一认证方(IdP):用户在 account.<域名> 登录一次后,访问其他接入的服务(如 Router 控制台、运营门户、工作台)免密直达;统一登出则一次退出所有接入服务。

访问入口

端点说明
https://account.<域名>/login门户登录页(account-portal SPA:账号密码 / 验证码 / 微信登录 + 注册入口)
https://account.<域名>/oauth/authorize授权端点(已登录则静默放行,未登录先到门户登录页)
https://account.<域名>/oauth/returntoSSO 回跳恢复(门户登录成功后续走 authorize;跳转目标由服务端签发验签,无开放跳转面)
https://account.<域名>/.well-known/jwks.json令牌验签公钥(接入方缓存,密钥轮换自动生效)
https://account.<域名>/oauth/logout统一登出(清所有接入服务的本地会话后回主站;未配置落地地址时回落本域落地页)。账号中心门户自身的「退出」在 SSO 开启时也走本端点
https://account.<域名>/loggedout已登出落地页(ACCOUNT_LOGOUT_REDIRECT_URL 未配置时的兜底)

接入方后端兑换令牌走集群内 http://account:8100/oauth/token,该路径不开放公网(部署验收项:公网访问 /oauth/token 应不可达)。

接入方接入步骤

  1. 部署即注册:account 启动时按 ACCOUNT_OAUTH_SEED_CLIENTS 声明幂等自动注册一等公民接入方(console 工作台、运营门户),部署清单是注册的唯一事实源,换环境无需任何手工步骤。声明为 JSON 数组(frontchannel_logout_uri 可省略;留空则不播种):

    yaml
    - name: ACCOUNT_OAUTH_SEED_CLIENTS
      value: >-
        [{"client_id":"router-console","name":"Router Console",
        "redirect_uri":"https://console.<域名>/oauth/callback",
        "frontchannel_logout_uri":"https://console.<域名>/oauth/logout"},
        {"client_id":"saas-ops","name":"Platform Portal",
        "redirect_uri":"https://platform.<域名>/oauth/callback"}]
    • 重复启动按声明整组覆盖(redirect_uri / 登出地址 / active),手工临时调整重启后回归声明值;
    • 需要直调 account 业务 API 的接入方(如工作台)在声明条目追加 "scope":"openid portal",兑换时同步下发业务令牌对;
    • 临时调试/第三方接入仍可用 CLI 单独注册(幂等,可重复执行更新):
    bash
    kubectl -n <namespace> exec deploy/account -- python -m app.manage register-client router-console \
      --name "Router Console" \
      --redirect-uri https://console.<>/oauth/callback \
      --frontchannel-logout-uri https://console.<>/oauth/logout \
      --scope openid
    • 回调地址按精确匹配校验(含 scheme/host/path/query),新增回调需再次注册。
  2. 接入方按 OIDC 授权码 + PKCE(S256) 流程接入:未登录 → 携 client_id / redirect_uri / code_challenge(S256) / state 跳转 /oauth/authorize → 回调拿 code → 集群内用 code_verifier 兑换 id_token → 验签后建本地会话。

    未登录时 authorize 302 到门户登录页(/login?sso=1,account-portal SPA);门户登录/注册/微信登录成功后自动经 /oauth/returnto 续走 authorize(returnTo 暂存于 HttpOnly 签名 cookie,跳转目标恒为服务端签发的 authorize URL),无需门户侧感知接入方。

  3. 登出:接入方提供单一「退出」入口,默认跳 https://account.<域名>/oauth/logout 统一登出(一次退出所有接入服务;SSO 未配置的私有化形态退化为仅清本域会话)。

部署配置

一键部署(deploy.sh)在 DEPLOY_PLATFORM=true 时自动创建 account 域 ingress(仅上表公网端点)。需在 deploy/ci/.env.local 补充两个新密钥:

bash
# 会话 cookie 签名密钥(与 JWT secret 相互独立)
ACCOUNT_SSO_COOKIE_KEY=$(openssl rand -base64 48)
# OIDC RS256 私钥(PEM 转单行,换行写成 \n 字面量)
ACCOUNT_SSO_OIDC_PRIVATE_KEY=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null \
  | awk '{printf "%s\\n", $0}')

两者经 veyraos-secret 注入 account Pod。任一缺失时服务启动会因安全校验直接失败(fail-closed,不允许默认密钥上线);补齐后重启 Pod 即恢复。

另有一项非密钥配置:ACCOUNT_LOGOUT_REDIRECT_URL(统一登出链终落地地址,如主站首页 https://www.<域名>;清单已内置占位值,留空则回落 /loggedout 落地页)。

Router 独立生产栈(deploy/router-prod/)同样部署 account 作 IdP:独立 namespace veyraos(与 router 面隔离),清单 account.yaml + account-ingress.yaml(公网面同样仅上表端点),密钥走 account-secret(键清单与生成命令见 deploy/router-prod/secret.example.yaml),域名经 apply.sh 占位符注入;console RP 的注册命令见该目录 README。

会话语义:服务端会话最长 7 天(到期需重新输密码);浏览器关闭后会话 cookie 即失效,重开浏览器访问任一接入服务会重新到登录页输一次密码。重置密码会使所有已登录会话立即失效。

Console(Router 控制台)本地会话角色在登录兑换时按 account 的 platform_staff 绑定判定:scopes 覆盖 router:operator → operator(运营面),无绑定 / 查询失败回落 caller(自助面),运营授权的撤销即时生效。

员工授权(platform_staff)

员工不是独立账号——account 注册用户 + 一条 staff 绑定 = 平台员工(#480 §二)。新部署的初始账号没有员工属性,首个员工(鸡生蛋问题)经一次性 CLI 自提:

bash
# 前置:该邮箱已在 account.<域名> 正常注册;幂等可重复执行
kubectl -n veyraos exec deploy/account -c account -- \
  python -m app.manage promote-staff <> --scopes staff:manage,saas:ops,router:operator

全新环境冷启动引导

全新环境没有用户、没有邮件通道,「注册要验证码 → 验证码要通道 → 配通道要员工登录 → 员工要注册」会死循环。破局点是部署期 env 邮件兜底(发送选路的末位虚拟通道,DB 无通道时自动生效):

  1. 部署时给 account 配 env 邮件兜底deploy-platform.yaml 已留注释组,解开填入真实值)。密码必须入 k8s Secret,不得明文进 shell 历史或 Deployment 清单:

    bash
    # ① 密码入 Secret(幂等,key 名即环境变量名,供 --from=secret 直接导入)
    kubectl -n veyraos create secret generic smtp-creds \
      --from-literal=ACCOUNT_SMTP_PASSWORD='<密码>' \
      --dry-run=client -o yaml | kubectl apply -f -
    
    # ② 其余非敏感项 + 从 Secret 导入密码(env 变更自动触发滚动重启)
    kubectl -n veyraos set env deploy/account \
      ACCOUNT_EMAIL_PROVIDER=smtp \
      ACCOUNT_SMTP_HOST=smtp.example.com \
      ACCOUNT_SMTP_PORT=465 \
      ACCOUNT_SMTP_USER=noreply@example.com \
      --from=secret/smtp-creds \
      ACCOUNT_SMTP_FROM=noreply@example.com
    • 云厂商发信(aliyun/tencent/huawei)改用 ACCOUNT_EMAIL_ACCESS_KEY_* 组,AK/SK 同样入 Secret,见 services/account/app/config.py
    • ACCOUNT_EMAIL_TEST_RECIPIENT 联调白名单生产必须留空——非空时仅白名单内目标能收到邮件,注册验证码会被拦。
  2. 注册首个账号:在 account.<域名> 用邮箱正常注册,验证码经 env 兜底通道送达。

  3. 提权首个员工:用上面的 promote-staff CLI。

  4. 登录运营门户配置正式通道:该员工 SSO 登录 platform.<域名> →「渠道配置」录入 DB 邮件通道(凭据加密入库,为主路径)。

  5. 收尾:env 兜底自动退居末位备份,可保留也可移除;后续通道变更一律走运营门户,不再动部署配置。

scope 目录(services/account/app/staff_scopes.py):

scope含义
staff:manage员工管理(运营门户「员工管理」页 + OAuth client 管理)
saas:opsSaaS 运营全集(细分 customers / credit / invoice / risk / channels / payments
router:operatorRouter 控制台运营面

首个员工自提后,加人/授权/回收/停用一律走运营门户「员工管理」页(需 staff:manage),变更落 account 审计;每请求查 staff 表校验,撤销即时生效。CLI 与 env 不做常驻提权通道(#480 拍板 f:一次性动作天然可审计)。

验收清单

  • 首次登录:接入方未登录 → 登录页 → 回调 → 进入业务页
  • 免密:已登录后访问第二个接入方,全程零交互
  • 重开浏览器:任一接入方触发重新输密码
  • 授权码单次有效:重复兑换被拒;篡改回调地址 / state / PKCE 均被拒
  • 统一登出:各接入方本地会话全部清除,链终落主站(未配置 ACCOUNT_LOGOUT_REDIRECT_URL 时为已登出落地页)
  • https://account.<域名>/oauth/token 公网不可达;JWKS 公网可读

基于内网部署的企业级 AI 智能体平台