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 的接入文档
This commit is contained in:
@@ -1,174 +1,316 @@
|
||||
# SSO-Mock → 真实后端 适配指南
|
||||
# SSO-Portal → 真实后端 适配指南
|
||||
|
||||
> 目标:当后端提供真实 SSO 接口后,把 sso-mock 从「自己伪造一次性 code」切换为「纯透传真实后端」。
|
||||
> 参考实现:生产前端 `ssofront`(`src/apis/login/index.ts`)已经在用这套契约,本指南以它为准。
|
||||
> 目标:后端已落地真实 SSO 接口。把 sso-portal 从「自造一次性 code + 内存 `codeStore`」切换为「纯透传真实后端」,external-app 改用**标准 OAuth2 授权码流程**换 token。
|
||||
> 本文以 **2026-07 后端提供的三个真实接口**为准。
|
||||
|
||||
> ⚠️ 与早期设想不同:真实后端**没有**采用「单端点 `auth/index` + `login_type` 区分」的设计,而是**按登录方式拆成独立端点**(`/sso/account`、`/sso/phone/smscode`),再用一个标准 OAuth2 端点(`/sso/access_token`)把授权 code 换成 access_token。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论:需要几个接口?
|
||||
## 1. 后端提供的三个真实接口
|
||||
|
||||
核心是 **2 个**(你已想到),完全对齐生产还需 **1 个辅助 + 2 个已有代理**:
|
||||
| # | 用途 | 接口(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` | **external-app(服务端)** | ✅ |
|
||||
|
||||
| # | 用途 | 后端接口(生产实测) | 必须 | 备注 |
|
||||
|---|------|----------------------|------|------|
|
||||
| ① | 登录 → 返回一次性 code | `POST /api/v1/auth/index` | ✅ | **单端点**,用 `login_type` 区分账号/手机/飞书 |
|
||||
| ② | 一次性 code → 用户信息 | `GET /api/v1/auth/inner_get_user_info?code=` | ✅ | 供 external-app 调用 |
|
||||
| ③ | app_id → 应用登录方式 | `GET /api/v1/application/by_client_id?client_id=` | ⭕ | 决定开放哪些登录 tab + 应用名;前端写死可省 |
|
||||
| ④ | 发送手机验证码 | `POST /api/v1/verify_code/send` | ⭕ | 手机登录才需要(sso-mock 现已代理为 basis 版) |
|
||||
| ⑤ | 租户简称列表 | `GET /api/v1/basis/tenant/base/listabbr` | ⭕ | 多租户选择(现已代理) |
|
||||
**宿主**:`http://172.16.115.31:6610`(即 `NEXT_PUBLIC_SHARED_API_PATH`)
|
||||
|
||||
> **一句话**:后端把 ①② 做出来,sso-mock 就能删掉内存 `codeStore` 与假 `/api/auth/verify`,变成纯代理。③ 是"完全对齐生产"的锦上添花。
|
||||
**固定凭据(SSO 应用注册值)**:
|
||||
|
||||
| 字段 | 值 | 持有方 |
|
||||
| --------------- | ---------------------------------- | ------------------------------- |
|
||||
| `client_id` | `6wds5qua3b76hu748zrjmall` | sso-portal(随 URL `app_id` 传入)+ external-app |
|
||||
| `client_secret` | `oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F` | **仅 external-app 服务端** env,绝不下发浏览器 |
|
||||
|
||||
**不变项**:
|
||||
- 发送手机验证码:依旧走 `POST /api/v1/basis/user/phone/code`(body `{ phone, tntkey }`),sso-portal 的 `app/api/getVerifyCode/route.ts` **不改**。
|
||||
- 飞书扫码:UI 已暂时隐藏,后端**未提供**对应 SSO 端点,不在本流程内。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口契约(字段级)
|
||||
|
||||
### ① 登录换 code — `POST /api/v1/auth/index`
|
||||
|
||||
请求体(三种方式共用,按 `login_type` 带不同字段):
|
||||
### ① 账号密码登录 — `POST /api/v1/basis/sso/account`
|
||||
|
||||
```jsonc
|
||||
// 请求体
|
||||
{
|
||||
"login_type": "password | phone | feishu", // 判别登录方式
|
||||
"client_id": "<app_id>", // 外部应用标识
|
||||
"client_redirect_uri": "<app_url>", // 外部应用回调地址
|
||||
|
||||
// login_type=password
|
||||
"phone": "+86138...", "password": "***",
|
||||
// login_type=phone
|
||||
"phone": "+86138...", "verify_code": "123456",
|
||||
// login_type=feishu
|
||||
"code": "<飞书授权码>", "redirect_uri": "<sso-mock/login 回调>"
|
||||
"account": "string",
|
||||
"password": "string",
|
||||
"tenant": "string", // 租户标识,取自 SSO_TENANT(默认 cowarobot)
|
||||
"client_id": "6wds5qua3b76hu748zrjmall",
|
||||
"client_redirect_uri": "<external-app>/callback"
|
||||
}
|
||||
// 响应
|
||||
{ "code": "<授权 code,一次性、短时效>" }
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{ "code": "一次性 sso_code(短时效、单次使用)" }
|
||||
```
|
||||
|
||||
### ② code 换用户信息 — `GET /api/v1/auth/inner_get_user_info?code=<sso_code>`
|
||||
|
||||
响应(`Login.UserInfo`,节选):
|
||||
### ② 手机验证码登录 — `POST /api/v1/basis/sso/phone/smscode`
|
||||
|
||||
```jsonc
|
||||
// 请求体
|
||||
{
|
||||
"token": "...", "refresh_token": "...",
|
||||
"user": {
|
||||
"id": 1, "name": "张三", "phone": "...",
|
||||
"permissions": { "/dashboard": ["view"] },
|
||||
"feishu": { "avatar_url": "..." }
|
||||
}
|
||||
"phone": "string", // 如 "+8613800000000"
|
||||
"smscode": "string", // 6 位短信验证码
|
||||
"tenant": "string",
|
||||
"client_id": "6wds5qua3b76hu748zrjmall",
|
||||
"client_redirect_uri": "<external-app>/callback"
|
||||
}
|
||||
// 响应
|
||||
{ "code": "<授权 code,一次性、短时效>" }
|
||||
```
|
||||
|
||||
### ③ 应用信息 — `GET /api/v1/application/by_client_id?client_id=<app_id>`
|
||||
> 验证码**发送**仍走 `/api/v1/basis/user/phone/code`(不变),与登录接口 `/sso/phone/smscode` 是两个不同端点,别混淆。
|
||||
|
||||
```json
|
||||
{ "data": { "providers": ["feishu", "password", "wechat"], "display_name": "XX 系统" } }
|
||||
### ③ 授权 code 换 token — `POST /api/v1/basis/sso/access_token`
|
||||
|
||||
```jsonc
|
||||
// 请求体
|
||||
{
|
||||
"client_id": "6wds5qua3b76hu748zrjmall",
|
||||
"client_secret": "oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F", // ⚠️ 仅服务端
|
||||
"code": "<①或②返回的授权 code>",
|
||||
"grant_type": "authorization_code", // 标准 OAuth2 取值
|
||||
"tenant": "string"
|
||||
}
|
||||
// 响应(按 OAuth2 习惯,以后端实测为准)
|
||||
{ "access_token": "...", "refresh_token": "...", "expires_in": 7200, "token_type": "Bearer", "user": { ... } }
|
||||
```
|
||||
|
||||
> ⚠️ **`code` 语义冲突**:basis 旧接口通常包一层业务外壳 `{ code: 200, data: {...}, message }`(此处 `code` 是**业务状态码**);而 ①②③ 里 `code` 是**授权码字符串**。落地时请以后端实测响应为准:若 ①② 返回的是平铺 `{ code: "<授权码>" }`,则直接取;若包了外壳 `{ code:200, data:{ code:"..." } }`,则取 `data.code`。下文代码示意两种都兼容。
|
||||
|
||||
---
|
||||
|
||||
## 3. 标准流程(切到真实后端后)
|
||||
## 3. 标准流程(OAuth2 授权码流程)
|
||||
|
||||
```
|
||||
external-app
|
||||
→ 跳转 sso-mock: /login?app_id=<id>&app_url=<encoded url>
|
||||
↓ 用户在 sso-mock 登录(账号/手机/飞书)
|
||||
→ sso-mock ① POST /api/v1/auth/index ⇒ { code }
|
||||
→ sso-mock 重定向: <app_url>?sso_code=<code>
|
||||
→ 跳转 sso-portal: /login?app_id=6wds5qua3b76hu748zrjmall&app_url=<external-app>/callback
|
||||
↓ 用户在 sso-portal 登录(账号 / 手机)
|
||||
→ sso-portal ① 或 ② 调 /sso/account | /sso/phone/smscode ⇒ 授权 code
|
||||
→ sso-portal 重定向: <app_url>?sso_code=<code>
|
||||
↓
|
||||
→ external-app ② GET /api/v1/auth/inner_get_user_info?code=<code> ⇒ { user, token }
|
||||
→ external-app 建立本地会话
|
||||
→ external-app /api/callback(服务端)③ POST /sso/access_token
|
||||
{ client_id, client_secret, code, grant_type, tenant } ⇒ access_token
|
||||
→ external-app 写 httpOnly Cookie,建立本地会话
|
||||
```
|
||||
|
||||
要点:
|
||||
- `client_secret` 只在 **external-app 服务端**出现,sso-portal 全程不持有。
|
||||
- 授权 `code` 一次性、短时效,**由后端保证**,sso-portal 不再自造、不再存内存。
|
||||
|
||||
---
|
||||
|
||||
## 4. sso-mock 需要改哪些文件
|
||||
## 4. sso-portal 需要改哪些文件
|
||||
|
||||
### 4.1 `app/api/login/route.ts` —— 从"伪造 code"改为"透传 auth/index"
|
||||
### 4.1 `app/api/login/route.ts` —— 改为透传 `/sso/*`,删除 `codeStore`
|
||||
|
||||
**现状**:调 basis 原始登录端点拿到 `user+access_token`,再 `codeStore.set()` 自造 code。
|
||||
**改为**:直接调 `/api/v1/auth/index`,把返回的 `code` 原样透传,**删除 codeStore 相关代码**。
|
||||
**现状**:调 basis 原始登录端点 → 拿 `user + access_token` → `codeStore.set()` 自造 code。
|
||||
**改为**:按 `type` 调对应 `/sso/*` 端点,把后端返回的授权 code 原样透传,**删除** `codeStore` / `generateCode`。
|
||||
|
||||
字段映射(前端解密后 body → 后端请求体):
|
||||
|
||||
| type | 前端字段 | 后端字段(`/sso/*`) |
|
||||
| -------- | -------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| account | `account`, `password`, `abbr`, `client_id`, `client_redirect_uri` | `account`, `password`, `tenant`(来自 `SSO_TENANT` env), `client_id`, `client_redirect_uri` |
|
||||
| phone | `phone`, `verify_code`, `tntkey`, `client_id`, `client_redirect_uri` | `phone`, `smscode`(=verify_code), `tenant`(来自 `SSO_TENANT` env), `client_id`, `client_redirect_uri` |
|
||||
|
||||
代码示意:
|
||||
|
||||
```ts
|
||||
// 三种方式统一映射到 auth/index
|
||||
const payloadMap = {
|
||||
account: { login_type: 'password', phone: body.phone, password: body.password },
|
||||
phone: { login_type: 'phone', phone: body.phone, verify_code: body.verify_code },
|
||||
feishu: { login_type: 'feishu', code: body.code, redirect_uri: body.redirect_uri },
|
||||
// ... decrypt() 不变,删除 import { codeStore } / generateCode ...
|
||||
|
||||
async function callSsoApi(path: string, body: object) {
|
||||
const res = await fetch(`${SHARED_API}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const text = await res.text()
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
throw new Error(`后端返回非 JSON [${res.status}]: ${text.slice(0, 200)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const decryptData = await req.json()
|
||||
const body = JSON.parse(decrypt(decryptData))
|
||||
const type = body?.type
|
||||
// tenant 由环境变量 SSO_TENANT 驱动(默认 cowarobot),ssoBody 显式使用、callSsoApi 纯透传
|
||||
const tenant = process.env.SSO_TENANT || 'cowarobot'
|
||||
|
||||
let ssoRes: any
|
||||
if (type === 'account') {
|
||||
ssoRes = await callSsoApi('/api/v1/basis/sso/account', {
|
||||
account: body.account,
|
||||
password: body.password,
|
||||
tenant,
|
||||
client_id: body.client_id,
|
||||
client_redirect_uri: body.client_redirect_uri,
|
||||
})
|
||||
} else if (type === 'phone') {
|
||||
ssoRes = await callSsoApi('/api/v1/basis/sso/phone/smscode', {
|
||||
phone: body.phone,
|
||||
smscode: body.verify_code,
|
||||
tenant,
|
||||
client_id: body.client_id,
|
||||
client_redirect_uri: body.client_redirect_uri,
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json({ error: `不支持的登录方式: ${type}` })
|
||||
}
|
||||
|
||||
// ⚠️ 删除旧的 `if (res?.code !== 200)` 判断 —— 现在 code 是授权码字符串,不是业务状态码。
|
||||
// 兼容平铺 / 外壳两种响应;失败时回显后端 message。
|
||||
const authCode = ssoRes?.code ?? ssoRes?.data?.code
|
||||
if (!authCode || typeof authCode !== 'string') {
|
||||
return NextResponse.json({
|
||||
error: ssoRes?.message || ssoRes?.msg || '登录失败,后端未返回授权 code',
|
||||
})
|
||||
}
|
||||
return NextResponse.json({ code: authCode })
|
||||
}
|
||||
const res = await callBasisApi('/api/v1/auth/index', {
|
||||
...payloadMap[type],
|
||||
client_id: body.client_id, // = app_id
|
||||
client_redirect_uri: body.client_redirect_uri, // = app_url
|
||||
})
|
||||
// 直接透传 code,不再 codeStore.set / generateCode
|
||||
return NextResponse.json({ code: res.code })
|
||||
```
|
||||
|
||||
> 注意:`client_id` / `client_redirect_uri` 现在是**必填**(一次性 code 必须绑定应用)。前端 `postLogin` / `qr-login.tsx` 已经在带这两个字段。
|
||||
> 前端 `components/login/index.tsx` 的 `postLogin()` 已携带 `client_id` / `client_redirect_uri`,**无需改动**。`handleLoginSuccess` 已按 `{ code }` 跳转,与新透传结构一致。
|
||||
|
||||
### 4.2 `app/api/auth/verify/route.ts` —— 从"读内存"改为"代理 inner_get_user_info"
|
||||
### 4.2 `utils/code-store.ts` —— 删除
|
||||
|
||||
**现状**:从 `codeStore` 取 user、自造 mock token。
|
||||
**改为**:
|
||||
授权 code 的签发 / 校验 / 一次性 / 过期全部由后端负责,内存 store 不再需要。同步移除 `login/route.ts` 里所有 `import { codeStore }`。
|
||||
|
||||
### 4.3 `app/api/auth/verify/route.ts` —— 删除
|
||||
|
||||
external-app 改为直连后端 `/sso/access_token`(见 §5),sso-portal 不再提供 code 换用户信息的代理。
|
||||
|
||||
### 4.4 `app/api/getVerifyCode/route.ts` —— 不变
|
||||
|
||||
继续代理 `POST /api/v1/basis/user/phone/code`(验证码**发送**逻辑沿用)。
|
||||
|
||||
---
|
||||
|
||||
## 5. external-app 需要改哪些文件
|
||||
|
||||
### 5.1 `app/api/callback/route.ts` —— 改为直连后端 `/sso/access_token`
|
||||
|
||||
**现状**:把 `sso_code` POST 给 sso-portal `/api/auth/verify` 换用户信息。
|
||||
**改为**:服务端直接调后端 `/api/v1/basis/sso/access_token`,带 `client_secret`,拿 token 写 httpOnly Cookie。
|
||||
|
||||
代码示意:
|
||||
|
||||
```ts
|
||||
const BACKEND = (process.env.SSO_BACKEND_API || 'http://172.16.115.31:6610').replace(/\/+$/, '')
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { code } = await req.json()
|
||||
if (!code) return NextResponse.json({ error: 'code 不能为空' }, { status: 400 })
|
||||
const res = await fetch(
|
||||
`${SHARED_API}/api/v1/auth/inner_get_user_info?code=${encodeURIComponent(code)}`
|
||||
)
|
||||
return NextResponse.json(await res.json(), { status: res.status })
|
||||
if (!code) {
|
||||
return NextResponse.json({ error: '缺少 code 参数' }, { status: 400 })
|
||||
}
|
||||
|
||||
const res = await fetch(`${BACKEND}/api/v1/basis/sso/access_token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
client_id: process.env.NEXT_PUBLIC_SSO_APP_ID, // 6wds5qua3b76hu748zrjmall
|
||||
client_secret: process.env.SSO_CLIENT_SECRET, // ⚠️ 仅服务端 env,不下发浏览器
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
tenant: process.env.SSO_TENANT || 'cowarobot',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: data?.message || data?.msg || '换 token 失败' },
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
|
||||
const token = data?.access_token ?? data?.data?.access_token
|
||||
const refreshToken = data?.refresh_token ?? data?.data?.refresh_token
|
||||
const user = data?.user ?? data?.data?.user
|
||||
|
||||
const response = NextResponse.json({ success: true })
|
||||
if (token) {
|
||||
response.cookies.set('token', token, { httpOnly: true, path: '/', maxAge: 86400 })
|
||||
}
|
||||
if (refreshToken) {
|
||||
response.cookies.set('refresh_token', refreshToken, {
|
||||
httpOnly: true, path: '/', maxAge: 86400 * 7,
|
||||
})
|
||||
}
|
||||
if (user) {
|
||||
response.cookies.set('user', JSON.stringify(user), { path: '/', maxAge: 86400 })
|
||||
}
|
||||
return response
|
||||
}
|
||||
```
|
||||
|
||||
> 或者让 external-app 直接调后端 ②,sso-mock 连这个代理都不用留。
|
||||
### 5.2 跳转链接 —— `app_id` 改为真实 client_id
|
||||
|
||||
### 4.3 `utils/code-store.ts` —— 删除
|
||||
|
||||
真实后端负责 code 的签发/校验/一次性/过期,内存 store 不再需要。同时移除 `route.ts` 里所有 `import { codeStore }`。
|
||||
|
||||
### 4.4 `app/api/getVerifyCode/route.ts` —— 换端点
|
||||
|
||||
`/api/v1/basis/user/phone/code` → `/api/v1/verify_code/send`(若后端统一到新契约)。
|
||||
|
||||
### 4.5(可选)应用信息驱动登录 tab
|
||||
|
||||
新增 `app/api/front/getAppInfo/route.ts` 代理 `③ by_client_id`,前端据 `providers` 动态渲染 tab(对齐 `ssofront/CloudLogin.tsx` 的 `asyncGetAppInfo`)。
|
||||
`app/DashboardPage.tsx` 跳 sso-portal 时 `app_id` 即 OAuth2 `client_id`,需改为 `6wds5qua3b76hu748zrjmall`,并把硬编码的 `localhost` 地址改为环境变量 `NEXT_PUBLIC_SSO_APP_ID` / `NEXT_PUBLIC_SSO_URL` / `NEXT_PUBLIC_APP_URL`(见 §6)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 环境变量
|
||||
## 6. 环境变量
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
NEXT_PUBLIC_SHARED_API_PATH=http://<真实后端>:<端口> # auth/index、inner_get_user_info 的宿主
|
||||
NEXT_PUBLIC_BASE_PATH= # 部署子路径(如有)
|
||||
```
|
||||
### sso-portal(`.env.development` / `.env.staging` / `.env.production`)
|
||||
|
||||
| 变量 | 说明 | dev / staging | production |
|
||||
| --------------------------------- | --------------------------------------------- | --------------------- | --------------------- |
|
||||
| `NEXT_PUBLIC_SHARED_API_PATH` | `/sso/*` 宿主 | `http://172.16.115.31:6610/` | `http://basis-app-svc:6610` |
|
||||
| `NEXT_PUBLIC_BASE_PATH` | 部署子路径,无则留空 | `""` | `""` |
|
||||
| `SSO_TENANT` | SSO 接口的租户标识(account / phone-smscode / access_token 共用) | `cowarobot` | `cowarobot` |
|
||||
|
||||
> sso-portal **不持有** `client_secret`(仅 external-app 服务端有);`SSO_TENANT` 驱动所有 SSO 接口的 `tenant` 字段。
|
||||
|
||||
### external-app
|
||||
|
||||
| 变量 | 端 | 说明 | 值 / 示例 |
|
||||
| -------------------------- | ------ | ----------------------------------------------------- | ---------------------------------- |
|
||||
| `NEXT_PUBLIC_SSO_APP_ID` | 两端 | OAuth2 `client_id`,作为 URL `app_id` 传给 sso-portal | `6wds5qua3b76hu748zrjmall` |
|
||||
| `NEXT_PUBLIC_SSO_URL` | 客户端 | sso-portal 登录页地址,客户端跳转 `/login` 用 | dev: `http://localhost:5501`,prod: `http://sso-portal-svc:5501` |
|
||||
| `NEXT_PUBLIC_APP_URL` | 客户端 | 本应用地址,作为 `client_redirect_uri` | dev: `http://localhost:4000` |
|
||||
| `SSO_CLIENT_SECRET` | 服务端 | `/sso/access_token` 鉴权用,**仅服务端** | `oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F` |
|
||||
| `SSO_BACKEND_API` | 服务端 | `/sso/access_token` 宿主 | dev: `http://172.16.115.31:6610`,prod: `http://basis-app-svc:6610` |
|
||||
| `SSO_TENANT` | 服务端 | 租户标识(与 sso-portal 的 `SSO_TENANT` 一致) | `cowarobot` |
|
||||
|
||||
> `SSO_CLIENT_SECRET` 含敏感信息,建议生产环境通过部署平台密钥注入,不入库;本地私有覆盖写入 `.env*.local`(已被 `.gitignore` 忽略)。`NEXT_PUBLIC_SSO_URL`(指向 sso-portal)用于客户端跳转 `/login`,故带 `NEXT_PUBLIC_` 前缀。
|
||||
|
||||
---
|
||||
|
||||
## 6. 迁移检查清单
|
||||
## 7. 迁移检查清单
|
||||
|
||||
- [ ] 后端就绪:`POST /api/v1/auth/index` 返回 `{ code }`
|
||||
- [ ] 后端就绪:`GET /api/v1/auth/inner_get_user_info` 返回用户信息
|
||||
- [ ] `login/route.ts` 改为调 auth/index 并透传 code,删 codeStore 逻辑
|
||||
- [ ] `auth/verify/route.ts` 改为代理 inner_get_user_info(或前端直连后端)
|
||||
- [ ] 删除 `utils/code-store.ts` 及其 import
|
||||
- [ ] `client_id` / `client_redirect_uri` 缺失时前端给出明确报错
|
||||
- [ ] (可选)接入 `by_client_id` 驱动登录 tab
|
||||
- [ ] 端到端验证:飞书回调只发 1 次 auth/index(已修复重复提交),换 code 成功,external-app 用 code 换到用户信息
|
||||
- [ ] 后端就绪:`/sso/account` 返回授权 code
|
||||
- [ ] 后端就绪:`/sso/phone/smscode` 返回授权 code
|
||||
- [ ] 后端就绪:`/sso/access_token` 返回 access_token
|
||||
- [ ] **确认 ①②③ 响应外壳**:`code` 是平铺授权码还是包在 `data.code`(决定解析方式)
|
||||
- [x] **`tenant` 取值已确认**:`cowarobot`(sso-portal 与 external-app 均用此值);**`grant_type`** 仍待确认(暂定 `authorization_code`)
|
||||
- [ ] sso-portal `login/route.ts`:改为透传 `/sso/account` / `/sso/phone/smscode`,删 `codeStore` / `generateCode`,去掉 `code !== 200` 判断
|
||||
- [ ] sso-portal 删除 `utils/code-store.ts`、`app/api/auth/verify/route.ts` 及相关 import
|
||||
- [ ] external-app `app/api/callback/route.ts`:改为服务端直连 `/sso/access_token`
|
||||
- [ ] external-app env:新增 `NEXT_PUBLIC_SSO_URL` / `SSO_CLIENT_SECRET` / `SSO_BACKEND_API` / `SSO_TENANT`,`NEXT_PUBLIC_SSO_APP_ID` 改为 `6wds5qua3b76hu748zrjmall`
|
||||
- [ ] `client_secret` 仅在 external-app 服务端 env,前端代码 / 网络请求中无泄露
|
||||
- [ ] 端到端:账号登录 → 拿 code → external-app 换 token → 建会话;手机登录同路径;验证码发送正常
|
||||
|
||||
---
|
||||
|
||||
## 7. 前端侧现状(无需改动)
|
||||
## 8. 安全注意
|
||||
|
||||
- `client_secret` **绝不下发浏览器**;`/sso/access_token` 一律由 external-app 的服务端 route handler 调用。
|
||||
- `access_token` / `refresh_token` 用 **httpOnly Cookie** 存储,前端不可读。
|
||||
- 授权 `code` 一次性、短时效,由后端签发与校验,sso-portal 不再自造、不缓存。
|
||||
- `client_redirect_uri` 必须与 SSO 注册的回调地址一致,防止开放重定向。
|
||||
|
||||
---
|
||||
|
||||
## 9. 前端侧现状(无需改动)
|
||||
|
||||
登录组件已满足新契约的调用要求:
|
||||
|
||||
- `components/login/index.tsx` `postLogin()`:账号/手机登录已携带 `client_id` / `client_redirect_uri`。
|
||||
- `components/login/components/login/qr-login.tsx`:飞书回调已带 `code / redirect_uri / client_id / client_redirect_uri`,并已加**一次性提交守卫**(`feishuLoginTriggered` ref)防止同一飞书 code 被并发重复提交。
|
||||
- `components/login/index.tsx` `postLogin()`:账号 / 手机登录已携带 `client_id` / `client_redirect_uri`,`handleLoginSuccess` 已按 `{ code }` 重定向回 external-app。
|
||||
- 飞书扫码 tab 已暂时隐藏(`Tabs.Tab value="feishu"` 注释 + `Tabs.Panel` 以 `{false && ...}` 屏蔽),恢复时取消对应注释即可。
|
||||
|
||||
Reference in New Issue
Block a user