SSO 单点登录与统一登出
account 服务作为 SaaS 产品线的统一认证方(IdP):用户在 account.<域名> 登录一次后,访问其他接入的服务(如 Router 控制台、运营门户、工作台)免密直达;统一登出则一次退出所有接入服务。
访问入口
| 端点 | 说明 |
|---|---|
https://account.<域名>/login | 门户登录页(account-portal SPA:账号密码 / 验证码 / 微信登录 + 注册入口) |
https://account.<域名>/oauth/authorize | 授权端点(已登录则静默放行,未登录先到门户登录页) |
https://account.<域名>/oauth/returnto | SSO 回跳恢复(门户登录成功后续走 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应不可达)。
接入方接入步骤
部署即注册: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 单独注册(幂等,可重复执行更新):
bashkubectl -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),新增回调需再次注册。
接入方按 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),无需门户侧感知接入方。登出:接入方提供单一「退出」入口,默认跳
https://account.<域名>/oauth/logout统一登出(一次退出所有接入服务;SSO 未配置的私有化形态退化为仅清本域会话)。
部署配置
一键部署(deploy.sh)在 DEPLOY_PLATFORM=true 时自动创建 account 域 ingress(仅上表公网端点)。需在 deploy/ci/.env.local 补充两个新密钥:
# 会话 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 自提:
# 前置:该邮箱已在 account.<域名> 正常注册;幂等可重复执行
kubectl -n veyraos exec deploy/account -c account -- \
python -m app.manage promote-staff <邮箱> --scopes staff:manage,saas:ops,router:operator全新环境冷启动引导
全新环境没有用户、没有邮件通道,「注册要验证码 → 验证码要通道 → 配通道要员工登录 → 员工要注册」会死循环。破局点是部署期 env 邮件兜底(发送选路的末位虚拟通道,DB 无通道时自动生效):
部署时给 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联调白名单生产必须留空——非空时仅白名单内目标能收到邮件,注册验证码会被拦。
- 云厂商发信(aliyun/tencent/huawei)改用
注册首个账号:在
account.<域名>用邮箱正常注册,验证码经 env 兜底通道送达。提权首个员工:用上面的
promote-staffCLI。登录运营门户配置正式通道:该员工 SSO 登录
platform.<域名>→「渠道配置」录入 DB 邮件通道(凭据加密入库,为主路径)。收尾:env 兜底自动退居末位备份,可保留也可移除;后续通道变更一律走运营门户,不再动部署配置。
scope 目录(services/account/app/staff_scopes.py):
| scope | 含义 |
|---|---|
staff:manage | 员工管理(运营门户「员工管理」页 + OAuth client 管理) |
saas:ops | SaaS 运营全集(细分 customers / credit / invoice / risk / channels / payments) |
router:operator | Router 控制台运营面 |
首个员工自提后,加人/授权/回收/停用一律走运营门户「员工管理」页(需 staff:manage),变更落 account 审计;每请求查 staff 表校验,撤销即时生效。CLI 与 env 不做常驻提权通道(#480 拍板 f:一次性动作天然可审计)。
验收清单
- 首次登录:接入方未登录 → 登录页 → 回调 → 进入业务页
- 免密:已登录后访问第二个接入方,全程零交互
- 重开浏览器:任一接入方触发重新输密码
- 授权码单次有效:重复兑换被拒;篡改回调地址 / state / PKCE 均被拒
- 统一登出:各接入方本地会话全部清除,链终落主站(未配置
ACCOUNT_LOGOUT_REDIRECT_URL时为已登出落地页) https://account.<域名>/oauth/token公网不可达;JWKS 公网可读