Files
sso-portal/ADAPTATION_GUIDE.md
zhangheng 37f10118d3 chore: initial commit of sso-mock SSO login service
Next.js + Mantine mock SSO service providing account/password, phone
verify-code, and Feishu QR login. Issues a one-time sso_code that the
external app exchanges for user info.

Includes:
- Feishu callback fix: guard against duplicate/concurrent /api/login
  submissions of the same single-use authorization code (feishuLogin
  once-guard + memoized onLoginSuccess to stop effect re-fire).
- ADAPTATION_GUIDE.md: how to switch from the in-memory code store to
  the real backend auth/index + inner_get_user_info endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:09:30 +08:00

6.7 KiB
Raw Blame History

SSO-Mock → 真实后端 适配指南

目标:当后端提供真实 SSO 接口后,把 sso-mock 从「自己伪造一次性 code」切换为「纯透传真实后端」。 参考实现:生产前端 ssofrontsrc/apis/login/index.ts)已经在用这套契约,本指南以它为准。


1. 结论:需要几个接口?

核心是 2 个(你已想到),完全对齐生产还需 1 个辅助 + 2 个已有代理

# 用途 后端接口(生产实测) 必须 备注
登录 → 返回一次性 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 多租户选择(现已代理)

一句话:后端把 ①② 做出来sso-mock 就能删掉内存 codeStore 与假 /api/auth/verify,变成纯代理。③ 是"完全对齐生产"的锦上添花。


2. 接口契约(字段级)

① 登录换 code — POST /api/v1/auth/index

请求体(三种方式共用,按 login_type 带不同字段):

{
  "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 回调>"
}

响应:

{ "code": "一次性 sso_code短时效、单次使用" }

② code 换用户信息 — GET /api/v1/auth/inner_get_user_info?code=<sso_code>

响应(Login.UserInfo,节选):

{
  "token": "...", "refresh_token": "...",
  "user": {
    "id": 1, "name": "张三", "phone": "...",
    "permissions": { "/dashboard": ["view"] },
    "feishu": { "avatar_url": "..." }
  }
}

③ 应用信息 — GET /api/v1/application/by_client_id?client_id=<app_id>

{ "data": { "providers": ["feishu", "password", "wechat"], "display_name": "XX 系统" } }

3. 标准流程(切到真实后端后)

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>
       ↓
  → external-app ② GET /api/v1/auth/inner_get_user_info?code=<code> ⇒ { user, token }
  → external-app 建立本地会话

4. sso-mock 需要改哪些文件

4.1 app/api/login/route.ts —— 从"伪造 code"改为"透传 auth/index"

现状:调 basis 原始登录端点拿到 user+access_token,再 codeStore.set() 自造 code。 改为:直接调 /api/v1/auth/index,把返回的 code 原样透传,删除 codeStore 相关代码

// 三种方式统一映射到 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 },
}
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 已经在带这两个字段。

4.2 app/api/auth/verify/route.ts —— 从"读内存"改为"代理 inner_get_user_info"

现状:从 codeStore 取 user、自造 mock token。 改为

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 })
}

或者让 external-app 直接调后端 ②sso-mock 连这个代理都不用留。

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.tsxasyncGetAppInfo)。


5. 环境变量

# .env.local
NEXT_PUBLIC_SHARED_API_PATH=http://<真实后端>:<端口>   # auth/index、inner_get_user_info 的宿主
NEXT_PUBLIC_BASE_PATH=                                 # 部署子路径(如有)

6. 迁移检查清单

  • 后端就绪: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 换到用户信息

7. 前端侧现状(无需改动)

登录组件已满足新契约的调用要求:

  • 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 被并发重复提交。