Files
sso-portal/INTEGRATION_GUIDE.md
zhangheng 775b4f6044 chore(sso-portal): 整合近期重构、UX 改进、部署脚本与文档
项目改名 sso-mock → sso-portal:
- 重命名仓库目录 sso-mock/ → sso-portal/
- package.json name 改 sso-portal
- app/layout.tsx title/description 改 SSO Portal
- 源码 [sso-mock] console.log 前缀、注释全部 → [sso-portal]
- 文档(README、ADAPTATION_GUIDE)、env 同步
- 生产 svc 名 sso-mock-svc → sso-portal-svc(external-app 引用同步)

SSO tenant 改环境变量驱动:
- 新增 SSO_TENANT env(默认 cowarobot)
- login/route.ts 顶部 const SSO_TENANT,ssoBody 显式使用
- callSsoApi 改回纯透传(不再硬编码覆盖 tenant)
- external-app 同步用 SSO_TENANT,命名对齐

external-app 登录态改为基于 token:
- page.tsx 读 token cookie 判定 authenticated(不再依赖 user cookie)
- DashboardPage 用 authenticated 决定跳转,user 为 null 时优雅兜底展示
- 解决「200 后又跳回 SSO」bug(access_token 接口不返回 user)

sso-portal 缺 SSO 参数体验优化:
- 缺 app_id/app_url 时 Alert 提示 + 禁用所有输入和登录按钮
- components/login/index.tsx 加 missingSsoParams 检测
- Alert 样式两排完整显示

部署脚本(参考 uirefbase):
- 新增 build.sh(git → pnpm install → build → docker build → tag → push)
- 新增 .gitlab-ci.yml(main 分支触发 build.sh prod)
- 新增 Dockerfile(standalone 模式,端口 5501)
- package.json 加 env-cmd 依赖 + build:stage/build 脚本
- next.config.ts 保持 output: 'standalone'

代码清理:
- 删除 utils/code-store.ts
- 删除 app/api/auth/verify/(连同目录)
- 移除旧的 codeStore / generateCode 逻辑

文档:
- ADAPTATION_GUIDE.md:完整重写对齐真实后端三个 SSO 接口(/sso/account、/sso/phone/smscode、/sso/access_token)
- 新增 INTEGRATION_GUIDE.md(从 external-app 迁移过来作为通用接入指南)
- README.md 同步更新(sso-portal 仓库门面 + 文档索引)
- external-app/README.md 加交叉链接指向 sso-portal 的接入文档
2026-07-08 20:53:51 +08:00

11 KiB
Raw Permalink Blame History

SSO 接入指南

适用:任何需要接入本 SSO 体系的前端应用Next.js / React / Vue / 其他均可参考)。 参考实现:external-app/Next.js 16完整可运行示例。 协议:标准 OAuth2 授权码流程


1. 前置准备

向 SSO 管理员申请以下信息,全部必填

字段 说明 示例
client_id OAuth2 应用标识(同时作为跳转 URL 的 app_id 6wds5qua3b76hu748zrjmall
client_secret 服务端密钥,⚠️ 仅服务端持有,绝不下发浏览器 oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F
tenant 租户标识 cowarobot
callback_url 你的应用回调地址,需与 SSO 注册完全一致 https://your-app.example.com/callback

还需确认两个地址:

地址 用途 示例
SSO_URL SSO 登录页地址(用于跳转 /login https://sso.softtest.cowarobot.com
BACKEND_API 后端 SSO 宿主(服务端调 access_token http://basis-app-svc:6610

2. 整体流程OAuth2 授权码)

[你的应用]                                          [sso-portal]              [后端 SSO]
   │                                                    │                       │
   │ ① 未登录302 跳转 SSO                             │                       │
   ├──────── /login?app_id=...&app_url=... ────────────▶│                       │
   │                                                    │                       │
   │                                                    │ ② 用户在 SSO 页登录  │
   │                                                    │   (账号/手机)         │
   │                                                    │                       │
   │ ③ SSO 302 回跳,带一次性 sso_code                  │                       │
   │◀──────── /callback?sso_code=xxx ───────────────────┤                       │
   │                                                    │                       │
   │ ④ 服务端用 sso_code + client_secret 换 token       │                       │
   ├──────────── POST /api/v1/basis/sso/access_token ───────────────────────────▶
   │◀─────────── { access_token, refresh_token, ... } ──────────────────────────┤
   │                                                    │                       │
   │ ⑤ 写 httpOnly Cookie重定向首页                   │                       │
   │  → 已登录 ✓                                        │                       │

关键点

  • 步骤 ④ 必须在你的服务端完成——client_secret 不能出现在前端代码/网络请求中。
  • sso_code 一次性、短时效,由后端保证,无需额外处理(不要重试同一 code
  • 步骤 ⑤ 写完 cookie 后即可视为登录成功。

3. 步骤详解

3.1 构造 SSO 登录跳转 URL

未登录时,跳转到 SSO

{SSO_URL}/login?app_id={client_id}&app_url={encodeURIComponent(callback_url)}

app_url 必须 encodeURIComponent

3.2 接收回调

callback_url 对应的页面/路由接收 sso_codeURL query 参数)。

校验:

  • sso_code 缺失 → 提示错误。
  • 存在 → 进入 3.3。

3.3 服务端换 token核心敏感操作

服务端调用后端:

POST {BACKEND_API}/api/v1/basis/sso/access_token
Content-Type: application/json

{
  "client_id": "<client_id>",
  "client_secret": "<client_secret>",
  "code": "<sso_code>",
  "grant_type": "authorization_code",
  "tenant": "<tenant>"
}

成功响应(当前契约,不含用户信息):

{
  "code": 200,
  "data": {
    "access_token": "zfa1eIUz0cDUTHA3RVvjjWIL3SbvBu2g",
    "expires_in": "7200",
    "refresh_token": "Fpczl1EreLWw3BKhw6ejqa93SchN1rBF"
  },
  "msg": ""
}

关于用户信息:标准 OAuth2 的 access_token 端点只返回 token不返回 user。如需展示用户信息,需让后端补一个「用 access_token 换用户信息」的 userinfo 端点。当前 external-app 的用户信息卡片会显示「后端未返回用户详情」占位。

Cookie 名 内容 属性
token access_token httpOnly, path=/, maxAge=86400(或与 expires_in 对齐)
refresh_token refresh_token(若有) httpOnly, path=/, maxAge=86400*7
user JSON.stringify(user)(若有) httpOnly(用于客户端展示)

3.5 登录态判定

token cookie 为准(拿到 access_token 即登录成功)。

  • 服务端组件 / SSR:直接读 token cookie 判定。
  • 客户端组件:让服务端把 authenticated: boolean 标志传下来(不要在前端读 httpOnly cookie——读不到
  • 避免依赖 user cookie 判定登录(后端不返回 user 就会误判为未登录)。

3.6 登出

清空 token / refresh_token / user cookiemaxAge=0),然后跳转回 {SSO_URL}/login?app_id=...&app_url=...


4. 关键配置项(环境变量)

下表是 Next.js 命名约定NEXT_PUBLIC_ 前缀 = 客户端可见;无前缀 = 仅服务端)。其他框架按各自约定映射(例如 React + 独立后端CLIENT_ID 可能由后端转发或注入 HTML

变量Next.js 说明
NEXT_PUBLIC_SSO_URL 客户端 SSO 登录页地址,构造跳转 URL 用
NEXT_PUBLIC_APP_URL 客户端 本应用地址,作为 client_redirect_uri
NEXT_PUBLIC_SSO_APP_ID 两端 OAuth2 client_id,同时作为 URL app_id
SSO_CLIENT_SECRET 仅服务端 client_secret绝不下发前端
SSO_BACKEND_API 服务端 后端 SSO 宿主(调 access_token
SSO_TENANT 服务端 租户标识(如 cowarobot

⚠️ SSO_CLIENT_SECRET 含敏感信息,生产建议通过部署平台密钥注入,不要入库。.env*.local 已被 .gitignore 忽略,本地私有覆盖请写入 .env*.local


5. 安全要求(必读)

  1. client_secret 绝不出现在前端代码、URL、浏览器可读 cookie 中。
  2. access_token / refresh_tokenhttpOnly Cookie,路径 /
  3. redirect_uri 必须与 SSO 注册完全一致(协议、域名、端口、路径都一致),否则换 token 失败。
  4. 授权 sso_code 一次性、短时效,由后端保证,前端不要重试。
  5. 生产环境 Cookie 应设置 SecureHTTPS和合理的 SameSiteLax/Strict
  6. 敏感日志(如 access_token 响应)生产环境应脱敏或关闭。

6. 参考实现external-appNext.js

完整可运行示例位于 sibling 仓库 ../external-app/

external-app/
├── app/
│   ├── page.tsx                  # 首页:服务端读 token cookie → authenticated → DashboardPage
│   ├── DashboardPage.tsx         # 登录后页面client 组件user 可为 null
│   ├── callback/
│   │   └── page.tsx              # 回调页:收 sso_code → POST /api/callback → 跳首页
│   └── api/
│       ├── callback/route.ts     # 服务端:调 access_token → 写 httpOnly cookie
│       └── logout/route.ts       # 清 cookie
├── .env.development
├── .env.staging
├── .env.production
└── README.md

关键代码片段(可直接对照阅读完整文件):

app/page.tsx(首页登录态判定):

const token = cookieStore.get('token')?.value
const authenticated = !!token
return <DashboardPage user={user} authenticated={authenticated} />

app/callback/page.tsx(回调):

const code = searchParams.get('sso_code')
await fetch('/api/callback', {
  method: 'POST',
  body: JSON.stringify({ code }),
})
window.location.href = '/'   // 成功 → 首页

app/api/callback/route.ts(服务端换 token + 写 cookie

const res = await fetch(`${BACKEND}/api/v1/basis/sso/access_token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    client_id, client_secret, code,
    grant_type: 'authorization_code',
    tenant: SSO_TENANT,
  }),
})
const { access_token, refresh_token } = await res.json()
response.cookies.set('token', access_token, { httpOnly: true, path: '/', maxAge: 86400 })
response.cookies.set('refresh_token', refresh_token, { httpOnly: true, path: '/', maxAge: 86400*7 })

7. 其他框架适配要点

React非 Next.js需独立 Node 后端)

  • 跳转/回调与 Next.js 同理,但 cookieStore 换后端读取(如 Express req.cookies)。
  • 客户端组件判定登录 → 通过后端 SSR 渲染时注入 authenticated,或客户端用 /api/me(轻量探活)查。
  • 跨域时 Cookie 需 SameSite=Lax/None; Secure

Vue / 静态前端 + 独立后端

  • 静态前端不能直接调 access_token(会暴露 client_secret),必须经后端中转。
  • 跳转/回调 HTML 端处理即可;后端提供换 token + 写 cookie 的接口。

跨域注意

  • SSO 与你的应用若不同域:后端换 token 后写 Cookie 需正确 Domain/Path/SameSite
  • sso-portal 与应用不同源时,跳转 URL 的 app_url 必须为完整可公网访问的 URL

8. 常见问题

现象 可能原因 / 排查
拿到 sso_code又跳回 SSO token cookie 未写入;服务端没读到 tokenauthenticated 未正确传给客户端
access_token 返回 401 client_secret 错;redirect_uri 与注册不一致;code 已用过 / 过期
redirect_uri 报错 与 SSO 注册的回调地址完全一致(含 http/https、端口、尾部斜杠)
tenant 报错 SSO_TENANT 与后端约定不符(当前约定 cowarobot
用户信息不展示 标准行为——access_token 不返回 user。后端补 userinfo 接口即可
同一 code 报「已使用」 一次性,由后端保证;不要在客户端重试
code !== 200 误判 切到 OAuth2 流程后,code 字段含义变了(业务码 vs 授权码),不要再用 code === 200 判断成功

9. 后端接口参考(速查)

# 用途 接口POST 调用方
账号密码登录 → 授权 code /api/v1/basis/sso/account sso-portal
手机验证码登录 → 授权 code /api/v1/basis/sso/phone/smscode sso-portal
授权 code → access_token /api/v1/basis/sso/access_token 你的应用服务端

③ 请求体:

{
  "client_id": "...",
  "client_secret": "...",
  "code": "...",
  "grant_type": "authorization_code",
  "tenant": "cowarobot"
}

10. 一句话总结

跳转 {SSO_URL}/login?app_id=...&app_url=... → 接 sso_code服务端client_secretaccess_token → 写 httpOnly cookie → 完成。

照此即可接入。有问题对照 external-app/ 实现,或查本仓库的 ADAPTATION_GUIDE.md