Skip to content

跨租户 SSO

跨租户 SSO 允许已在 公司 A 注册的用户,使用同一套凭证关联账号并登录 公司 B。该能力适用于不同租户之间,也适用于同一租户内母公司与其子公司之间。身份通过 GlobalIdentity 层用邮箱或手机号统一,各门户的业务数据(代币、礼券等)仍严格隔离。所有已关联门户共享同一个链上钱包。

概览

说明
范围仅会员端
身份键邮箱或手机号
会话载体HttpOnly Cookie(vio_sso_token
令牌模型JWT access/refresh 仍按门户划分(每个「租户 + 子公司」组合一套)
钱包所有已关联门户共用同一链上钱包地址
数据隔离每个门户(租户 + 子公司)有各自的 User 与业务数据
门户键门户由复合键 (tenantId, subCompanyId) 标识。母公司为 subCompanyId = null

架构

┌──────────────┐   cookie    ┌─────────────────┐   cookie    ┌──────────────┐
│  Company A   │ ──────────► │  SSO Session    │ ◄────────── │  Company B   │
│  Member App  │             │  (Global)       │             │  Member App  │
└──────┬───────┘             └────────┬────────┘             └──────┬───────┘
       │                              │                             │
       │ JWT (portal-a)               │ GlobalIdentity              │ JWT (portal-b)
       │                              │                             │
       ▼                              ▼                             ▼
  User (portal-a)            Email / Phone              User (portal-b)
       │                     Password (hashed)                │
       └────────────────► Shared Wallet Address ◄─────────────┘

「门户」指租户与子公司的唯一组合。例如租户 A 的母公司与租户 A 的子公司 X 在同一租户下是两个不同门户。用户可在不同租户间 SSO,也可在同一租户的母公司与子公司间 SSO。

核心模型

模型作用
GlobalIdentity保存统一邮箱/手机 + 哈希密码。通过 linkedTenants[] 关联各门户的 User,每项含 tenantIdsubCompanyIduserId
SSOSession表示活跃的全局登录会话,令牌存在 HttpOnly Cookie 中。
User.globalIdentityId门户内用户指向其全局身份的引用。
User.walletAddress共享的链上钱包地址,所有已关联用户指向同一地址。

按门户区分的身份

每个门户由其租户及子公司(如有)唯一标识。同一个人可在以下场景拥有不同的会员资料:

  • 不同租户(如公司 A 与公司 B)
  • 同一租户的母公司与子公司(如公司 A 母公司与公司 A / 分店 X)
  • 同一租户内不同子公司(如公司 A / 分店 X 与公司 A / 分店 Y)

用户流程

1. 首次注册(公司 A)

  1. 用户在公司 A 注册。
  2. 后端创建 GlobalIdentity(若为新)并关联公司 A 的 User
  3. 创建 SSOSession,令牌写入 HttpOnly Cookie。
  4. 用户获得公司 A 的租户级 JWT。
  5. 创建托管链上钱包,地址写入 User

2. 跨租户与跨子公司场景

以下情形同时适用于跨租户(不同租户)与跨子公司(同租户不同子公司)。流程一致 — 系统检测到门户不一致后提示用户关联。

用户关联新公司或新子公司的路径有三类主干场景(外加母公司门户与 E/D 补充):

  1. 用户打开公司 B 会员端,进入 Sign Up
  2. 输入的邮箱或手机号已在公司 A 注册。
  3. 前端调用 POST /api/auth/sso/check-identifier 检测跨租户账号。
  4. 弹出提示:「您已使用此账号在公司 A 注册。是否将账号关联到公司 B,并用同一凭证登录?」
  5. 用户点 Yes 则跳转到公司 B 的 Login
  6. 用户输入相同邮箱与密码,点 Log In
  7. 后端检测到租户不一致,且用户已确认关联,则在公司 B 新建 User、关联 GlobalIdentity,并共用已有钱包地址。
  8. 登录成功,确认弹窗:「已成功将账号关联到公司 B。」
  9. 用户可开始使用公司 B 会员端。
  1. 用户在公司 A 已建立 SSO Cookie 的前提下打开公司 B 的 URL。
  2. SSOProvider 自动调用 GET /api/auth/sso/check,发现已有全局身份。
  3. 若已关联且有效:通过换票自动登录(无弹窗)。
  4. 若尚未关联或需新资料:自动出现与场景 A 相同的弹窗(无需先输入邮箱)。
  5. 用户点 Yes 后跳转公司 B 登录页,后续从场景 A 第 6 步起相同。

场景 C — 在公司 B 直接登录(不走注册)

  1. 用户进入公司 B Login,输入公司 A 的凭证。
  2. 后端在公司 A 找到用户、校验密码,发现租户不一致。
  3. 返回含 crossTenantRequired 的响应及来源公司名称。
  4. 弹窗:「您已使用此账号在公司 A 注册。是否将账号关联到公司 B?」
  5. 用户点 Yes,客户端用 crossTenantLink: true 重发登录请求。
  6. 后端在公司 B 创建用户、关联 GlobalIdentity、共享钱包。
  7. 登录成功,确认弹窗:「已成功将账号关联到公司 B。」
  8. 用户可使用公司 B 会员端。

场景 E — 仅从子公司 URL 注册用户访问母公司 URL(同租户)

  1. 用户公司 A / 分店 X 注册,访问公司 A 母公司会员端登录 URL(路径中无子公司段)。
  2. 后端可能返回 parentPortalLinkRequiredsourceSubCompanyName,或在已存在关联的母公司资料时直接解析。
  3. 用户确认关联后,客户端以 crossParentPortalLink: true 重试。
  4. 后端创建或关联租户级用户(subCompanyId 为 null)、关联 GlobalIdentity,并返回母公司门户 JWT。

场景 D — 在子公司 URL 直接登录(同租户)

  1. 用户在公司 A 母公司门户注册,进入 公司 A / 分店 X 登录页并输入凭证。
  2. 后端在母公司找到用户、校验密码,发现为同租户不同子公司(门户不一致)。
  3. 返回 crossSubCompanyRequired 及来源子公司名称。
  4. 弹窗:「您已在公司 A 注册。是否将账号关联到分店 X?」
  5. 用户点 Yes,请求带上 crossSubCompanyLink: true 重发。
  6. 后端在公司 A 下新建 subCompanyId 为分店 X 的 User,关联同一 GlobalIdentity,共享钱包。
  7. 登录成功。
  8. 用户可使用分店 X 会员端。
mermaid
flowchart TD
    subgraph scenarioA ["场景A:注册(无Cookie)"]
        A1["用户在公司B注册页输入邮箱"] --> A2["API: POST /auth/sso/check-identifier"]
        A2 --> A3{"其他租户已存在?"}
        A3 -->|是| A4["弹窗:已在公司A注册"]
        A4 -->|是| A5["跳转公司B登录"]
        A5 --> A6["输入凭证并登录"]
        A6 --> A7["API: POST /auth/login crossTenantLink=true"]
        A7 --> A8["后端建User、关联身份、共用钱包"]
        A8 --> A9["确认:已关联"]
        A3 -->|否| A10["正常注册"]
    end

    subgraph scenarioB ["场景B:有Cookie"]
        B1["用户访问公司B"] --> B2["SSOProvider: GET /auth/sso/check"]
        B2 --> B3{"发现SSO会话?"}
        B3 -->|"已关联"| B7["自动SSO登录"]
        B3 -->|"未关联/需新"| B4["自动弹窗"]
        B4 -->|是| B5["跳转公司B登录"]
        B5 --> B6["与场景A第6步起相同"]
    end

    subgraph scenarioC ["场景C:直接登录"]
        C1["在公司B登录用公司A凭证"] --> C2["API: POST /auth/login"]
        C2 --> C3{"租户不一致?"}
        C3 -->|是| C4["返回 crossTenantRequired"]
        C4 --> C5["弹窗:已在公司A注册"]
        C5 -->|是| C6["重发登录 crossTenantLink=true"]
        C6 --> C7["同场景A第8步后"]
    end

    subgraph scenarioD ["场景D:跨子公司登录"]
        D1["在分店X登录用母公司凭证"] --> D2["API: POST /auth/login"]
        D2 --> D3{"同租户子公司不一致?"}
        D3 -->|是| D4["返回 crossSubCompanyRequired"]
        D4 --> D5["弹窗:已在母公司注册"]
        D5 -->|是| D6["重发登录 crossSubCompanyLink=true"]
        D6 --> D7["后端在分店X建User并关联"]
        D7 --> D8["登录成功"]
    end

3. 切换账号

用户已将账号关联到多个公司或子公司后,有两种切换方式:

首页快捷切换

  1. Home 页点击左上角 头像
  2. 底部弹层列出所有已关联门户(公司与子公司)。
  3. 子公司条目显示为 「公司名称 — 子公司名称」
  4. 当前门户带 “Current” 标记。
  5. 点击其他门户即切换。
  6. 应用导航到该门户 URL,经 SSO 自动登录。

切换提示

多个账号关联时,头像上会显示小型切换图标,表示可使用快捷切换。

「关联账号」完整页

  1. 进入会员端 Account
  2. 点击 Linked Accounts 查看全部已关联门户。
  3. 每条显示公司名称(及适用的子公司名)。
  4. 在非当前门户上点击 Switch
  5. 应用跳转到该门户 URL(如 /{company-slug}//{company-slug}/{sub-company-slug}/)。
  6. SSOProvider 读取 Cookie,发现已关联账号并自动登录。
  7. 用户进入另一门户的应用界面(品牌与数据均为该门户)。

在此页也可对不再需要的门户执行 unlink(解绑)。

钱包共享

用户在不同门户(跨租户或跨子公司)关联后,所有关联的 User 记录共享同一链上钱包地址。即:

  • 任一门户获得的代币进入同一钱包。
  • 钱包仅在首次注册时创建一次。
  • 钱包仅在首次注册时创建一次。
  • 关联到额外门户时,系统会复用已有钱包地址,而不会新建钱包。

数据隔离

SSO 在门户间共享钱包地址登录凭证,其余业务数据按门户严格隔离:

  • 会员资料(各公司或子公司独立)
  • 代币余额、礼券、交易与活动
  • 推送订阅与偏好设置
  • 门店关联与会员等级

即使账号已关联,各门户的数据仍彼此独立。

面向集成方

SSO 相关 API 的完整请求/响应说明见 对外 API 参考

编程式跨租户关联(External API)

除了上述会员端交互式 SSO 流程外,External API 还提供编程式用户创建及自动跨租户关联:

POST /api/external/v1/users/find-or-create

该幂等端点处理三种场景:

  1. 用户已存在于当前门户 → 返回已有用户(created: false, linked: false
  2. 用户存在于其他租户 → 在当前门户创建关联 profile,共享显示名称、头像和钱包(created: true, linked: true
  3. 全新用户 → 创建新用户和 GlobalIdentity(created: true, linked: false

从外部系统(CRM、POS 等)同步用户时,推荐使用此端点 — 安全地处理跨租户身份关联,无需交互式 SSO 确认流程。

详见 对外 API 参考 的完整请求/响应说明。

安全说明

  • 关联新门户前,会员必须主动确认,系统才会共享凭证。
  • 修改密码后,所有共享同一全局身份的已关联门户均会生效。
  • 会员不能解绑最后一个仍保留的关联门户。

VIO v4 平台文档