# 外部单点登录(External SSO)接入说明 ## 功能概述 - 支持外部系统携带一次性 token 跳转本系统,实现免密单点登录。 - 去除了历史实现中的 payload 解密、nonce 强校验、自动建号与邮箱匹配等逻辑,所有账号均需在本地预先存在并保持映射关系。 - 前后端新增 `/system/sso/verify` 校验能力,返回标准的 `AuthLoginRespVO` 令牌信息,并记录审计与登录日志。 - 通过 `ExternalSsoClient` 抽象外部接口调用,可按需自定义实现或复用默认 HTTP 客户端封装。 ## 关键组件 ### 后端 - `ExternalSsoServiceImpl`:单点登录主流程,实现参数校验、外部用户查询、本地账号匹配、令牌签发与日志记录。 - `ExternalSsoStrategy`:定义按来源系统拆分的策略接口,不同系统可实现自定义的拉取与匹配逻辑。 - `DefaultExternalSsoStrategy`:默认策略实现,复用配置化的 HTTP 客户端与匹配顺序,可按优先级被自定义策略覆盖。 - `ExternalSsoClient`:获取外部用户信息的接口抽象。 - `DefaultExternalSsoClient`:基于 `RestTemplate` 的默认实现,支持 Header/Query/Body 占位符渲染、重试、响应字段映射、代理配置。 - `ExternalSsoClientConfiguration`:通过 `@Configuration` 在缺省情况下注册 `DefaultExternalSsoClient` Bean,允许业务自定义覆盖。 - `ExternalSsoProperties`:`external-sso.*` 配置项,包含开关、外部接口、账号映射、跨域等子配置;示例配置已同步到 `zt-module-system/zt-module-system-server/src/main/resources/application.yaml` 与 `zt-server/src/main/resources/application.yaml`。 - `ExternalSsoVerifyReqVO`:`POST /system/sso/verify` 请求载荷。 - `ExternalSsoUserInfo`:外部用户标准化模型,包含基础字段与自定义属性集合。 ### 前端 - `src/router/modules/remaining.ts`:新增隐藏路由 `/externalsso`,用于回调页面。 - `src/views/Login/ExternalSsoCallback.vue`:处理 URL 参数、调用校验接口、落地令牌、跳转目标页面,异常时提示并引导返回登录页。 - `src/api/login/index.ts`:新增 `externalSsoVerify` 方法,请求 `/system/sso/verify` 并返回 `TokenType`。 ## 调用流程 1. 外部系统完成本地认证后,构造 URL:`{本系统域名}/#/externalsso?x-token={外部token}&target={回跳地址}` 并跳转,可选附带 `sourceSystem` 指定来源系统。 2. Vue 页面 `ExternalSsoCallback` 解析查询参数,优先读取 `x-token`(兼容历史的 `token` 参数)并校验是否存在;缺失时终止流程并提示错误。 3. 前端调用 `POST /system/sso/verify`,请求体: ```json { "token": "外部系统颁发的 token", "targetUri": "/#/dashboard", // 可选 "sourceSystem": "partner-a" // 可选 } ``` 4. `ExternalSsoServiceImpl#verifyToken` 执行以下步骤: - 校验功能开关与 token 有效性。 - 基于 `sourceSystem` 选择匹配的 `ExternalSsoStrategy`,若无匹配直接返回来源系统不支持。 - 通过策略触发 `ExternalSsoClient` 拉取外部用户信息(默认调用配置的 HTTP 接口)。 - 按策略内定义的匹配顺序(默认外部 ID → 用户名 → 手机号)查找本地账号,未命中直接返回 "未找到匹配的本地用户"。 - 校验账号状态是否启用,签发 OAuth2 访问令牌并记录登录日志(类型 `LOGIN_EXTERNAL_SSO`)。 - 生成操作审计日志,记录外部响应摘要、映射账号、目标地址、来源系统等信息。 5. 前端拿到 `AuthLoginRespVO`,通过 `authUtil.setToken`/`setTenantId` 持久化,随后跳转到整理后的 `targetUri`(默认 `/`)。 6. 当流程出现异常(例如 token 缺失、外部接口失败、账号不存在)时,后端返回对应错误码,前端弹出提示并清除本地缓存。 下图为时序示意: ```text 外部系统 -> 浏览器 -> Vue /externalsso -> POST /system/sso/verify -> ExternalSsoService -> ExternalSsoClient -> 外部用户接口 \-> OAuth2TokenService -> 登录日志/审计日志 ``` ## 前端细节 - `ExternalSsoCallback.vue` 对 `target`/`targetUri`/`redirect` 参数做统一解码与归一化,支持携带绝对地址或 hash 路由,防止开放跳转漏洞。 - 解析 URL 中的 `sourceSystem`(兼容 `source`、`systemCode`)并透传给后端,便于在多来源系统场景下选择策略。 - 在成功回调后调用 `router.replace`,保证不会产生历史记录;失败时引导用户回到 `/login` 并附带原目标地址。 - 通过 `buildErrorMessage` 兼容后端返回的 `msg`、`Error` 对象或字符串,统一展示错误提示。 ## 后端流程拆解 - **开关与参数校验**: - 关闭开关或缺少 token 时抛出 `EXTERNAL_SSO_DISABLED` / `EXTERNAL_SSO_TOKEN_MISSING`。 - **外部用户获取**: - 默认客户端会在 `external-sso.remote` 中加载请求配置,支持 GET/POST 等多种场景。 - 占位符:`${externalUserId}`(初始值为 token)、`${shareToken}`/`${token}`(共享服务访问 token)、`${xToken}`(原始回调 token)、`${targetUri}`、`${sourceSystem}`。 - 会自动通过 `ShareServiceUtils` 获取共享服务访问 token,并写入 `ShareServiceProperties#tokenHeaderName` 对应的请求头。 - 请求体会以 JSON 形式发送 `{ "x-token": "回调参数中的 token" }`,满足上游接口 `S_BF_CS_01` 的要求。 - `validateResponse` 可按 `codeField` 与 `successCode` 校验业务状态,失败时抛出带详细信息的 `ExternalSsoClientException`。 - **本地账号匹配**: - 使用 `mapping.order` 控制字段优先级;`custom.entries` 保存 "外部ID → 本地用户ID" 静态映射。 - 找不到用户或账号禁用时分别抛出 `EXTERNAL_SSO_USER_NOT_FOUND`、`EXTERNAL_SSO_USER_DISABLED`,均会同步写入登录日志。 - **令牌签发与日志**: - 通过 `OAuth2TokenService#createAccessToken` 使用默认客户端 `CLIENT_ID_DEFAULT` 颁发本地访问令牌。 - `recordAuditLog` 把原始响应的 SHA-256 摘要、外部属性、token 摘要等写入操作日志,便于排查。 - `recordLoginLog` 记录登录行为并在成功时更新用户最后登录 IP。 ## `ExternalSsoClient` 扩展 - 默认实现 `DefaultExternalSsoClient` 由 `ExternalSsoClientConfiguration` 自动注册,若需要接入其它协议,可在任意配置类中自定义: ```java @Bean public ExternalSsoClient customExternalSsoClient(...) { return new MyExternalSsoClient(...); } ``` - 默认实现要点: - 按配置构造 `RestTemplate`,支持连接/读取超时、HTTP 代理、重试次数。 - 解析 JSON 响应,将指定字段映射到 `ExternalSsoUserInfo`,并保留原始 data 节点到 `attributes`。 - 在解析失败、状态码异常时抛出带原始响应的 `ExternalSsoClientException`。 ## 配置项参考 ```yaml external-sso: enabled: true system-code: example-partner token: secret: "shared-secret" algorithm: AES allowed-clock-skew-seconds: 60 max-age-seconds: 300 require-nonce: false replay-protection-enabled: false remote: base-url: http://10.1.7.110 user-info-path: /api/sso/user method: POST headers: Authorization: "Bearer ${token}" query-params: {} body: userId: "${externalUserId}" code-field: code success-code: "0" message-field: message data-field: data user-id-field: data.userId username-field: data.username nickname-field: data.nickname email-field: data.email mobile-field: data.mobile tenant-id-field: data.tenantId connect-timeout-millis: 3000 read-timeout-millis: 5000 retry-count: 1 proxy: enabled: false mapping: order: - EXTERNAL_ID - USERNAME - MOBILE ignore-case: true update-profile-on-login: false custom: entries: partnerUser001: 10001 cors: allowed-origins: - https://partner.example.com allowed-methods: ["OPTIONS", "POST"] allowed-headers: ["Authorization", "Content-Type"] allow-credentials: true max-age: 1800 ``` | 配置路径 | 说明 | | --- | --- | | `enabled` | 总开关,关闭后接口直接返回 `EXTERNAL_SSO_DISABLED` | | `system-code` | 默认来源系统标识,可作为 `sourceSystem` 的缺省值及日志标签 | | `token.*` | 若仍需解密/校验外部 token,可在自定义 `ExternalSsoClient` 内按需使用;默认实现仅透传 | | `remote.*` | 外部接口 HTTP 调用参数、字段映射与超时控制,模板占位符支持 `externalUserId`、`shareToken`(`token`)、`xToken`、`targetUri`、`sourceSystem` | | `mapping.order` | 本地账号匹配优先级,支持 `EXTERNAL_ID`、`USERNAME`、`MOBILE` | | `mapping.custom.entries` | 外部用户标识到本地用户 ID 的静态映射表 | | `cors.*` | 用于开放 `/system/sso/verify` 的跨域访问白名单 | ## 错误码与日志 - 错误码: - `1_002_000_050`:功能未开启。 - `1_002_000_051`:token 缺失。 - `1_002_000_055`:外部接口异常,具体原因写入占位符。 - `1_002_000_056`:未匹配到本地用户。 - `1_002_000_057`:本地用户已禁用。 - `1_002_000_058`:来源系统不支持,需配置匹配的策略实现。 - 登录日志:使用 `LoginLogTypeEnum.LOGIN_EXTERNAL_SSO` 记录成功/失败。 - 操作日志:类型 `EXTERNAL_SSO/VERIFY`,包含外部用户 ID、映射账号、目标地址、来源系统、响应摘要等元数据。 ## 注意事项 - 所有账号须提前维护映射,系统不会自动创建或按邮箱兜底匹配用户。 - `targetUri` 会在前端归一化,避免开放跳转风险;无合法目标时默认跳转首页。 - 确保外部接口返回的 JSON 字段与配置保持一致,必要时可通过 `remote.data-field` 指向具体节点。 - 若外部接口速度较慢或易失败,可提高 `retry-count`、超时时间或自定义客户端实现。 - 如需记录更多审计信息,可在 `ExternalSsoUserInfo#addAttribute` 注入自定义字段,审计日志会自动保留。 ## 扩展与测试建议 - 通过提供新的 `ExternalSsoStrategy` 或 `ExternalSsoClient` Bean,可扩展不同来源系统的对接方式。 - 建议为主要错误场景编写集成测试:token 缺失、映射缺失、来源系统不支持、外部接口超时、账号被禁用等。 - 外部系统回调前可先调用 `/system/sso/verify` 联调接口验证配置是否正确,再接入正式流程。