skillhub/docs/12-private-sso-integration-playbook.md

15 KiB
Raw Permalink Blame History

私有 SSO 接入兼容层实施手册

1. 文档目的

本文档面向两类读者:

  • 后续在私有仓库中接入企业 SSO 的开发者
  • 需要基于当前开源版兼容层继续开发的 coding agent

本文档不是认证架构总览,而是实施手册。目标是让后续执行者在不了解全部历史上下文的情况下,也能基于当前成果直接开始接入工作,并且尽量把私有仓库与开源仓库的差异控制在 provider 实现层和少量配置层。

相关文档:

2. 当前上下文与已确认约束

本轮改造的真实目标不是在开源版里实现私有 SSO而是先把开源版前后端改造成一个稳定的兼容接入层。

已经确认的业务前提如下:

  • 私有 SSO 能返回稳定且唯一的 UID
  • 用户名密码校验接口与基于 Cookie 的会话校验接口都返回同一个 UID
  • SkillHub 私有版与私有 SSO 会部署在统一主域下,例如 skill.xxx.comsso.xxx.com
  • 私有版可以通过内部接口或 RPC 调用 SSO 的用户名密码校验能力
  • 首次 SSO 登录自动创建 SkillHub 账号
  • 不考虑账号合并
  • 不依赖 email 字段
  • 不要求联动登出,但可保留低优先级扩展点

这意味着后续私有 SSO 的正确接入方式是:

  • 把 SSO 建模为新的认证来源 private-sso
  • providerCode + subject 表示外部身份,其中 subject 就是 SSO UID
  • 复用当前平台的统一 Session 建立逻辑,而不是再造一套登录态机制

3. 当前兼容层已经提供了什么

3.1 后端扩展点

当前开源版已经提供以下后端兼容能力:

  • DirectAuthProvider
    • 用于“前端收集用户名密码,后端调用外部系统校验”的模式
  • PassiveSessionAuthenticator
    • 用于“浏览器自动带上 SSO Cookie后端读取请求并向 SSO 校验”的模式
  • PlatformSessionService
    • 用于统一建立 SkillHub Web Session
  • LogoutPropagationHandler
    • 用于未来低优先级登出联动

关键代码位置:

3.2 后端公共协议

当前开源版已经提供以下兼容协议:

  • POST /api/v1/auth/direct/login
  • POST /api/v1/auth/session/bootstrap
  • GET /api/v1/auth/methods

这些协议的设计原则如下:

  • 默认关闭
  • 默认没有私有 SSO 实现
  • 启用后由 provider 扩展驱动
  • 成功后统一建立标准 Spring Security Session
  • 不替换现有 /api/v1/auth/local/login
  • 不替换现有 OAuth 登录

3.3 前端兼容层

当前开源版前端已经支持通过运行时配置开启兼容入口:

  • SKILLHUB_WEB_AUTH_DIRECT_ENABLED
  • SKILLHUB_WEB_AUTH_DIRECT_PROVIDER
  • SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLED
  • SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDER
  • SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO

前端设计原则如下:

  • 默认不启用任何私有登录入口
  • 开启后通过兼容层切换,不破坏现有登录页默认行为
  • 优先走统一目录接口 /api/v1/auth/methods
  • 被动会话登录优先使用显式 bootstrap而不是页面加载时偷偷尝试多次

4. 私有 SSO 的推荐接入方案

4.1 推荐总策略

最佳实践不是只选一种方式,而是同时支持两条链路:

  1. 主路径:DirectAuthProvider

    • 登录页展示企业 SSO 用户名密码表单
    • 后端通过内部接口或 RPC 调用私有 SSO 校验
    • 校验成功后给用户建立 SkillHub Session
  2. 补充路径:PassiveSessionAuthenticator

    • 当用户已经在 SSO 系统登录过,并且浏览器会自动带上共享 Cookie 时
    • 登录页允许用户主动点击“从企业 SSO 登录”
    • 或在非常谨慎的前提下自动尝试一次 bootstrap

这样做的理由:

  • 覆盖“尚未登录 SSO”和“已登录 SSO”两种用户状态
  • 不依赖浏览器一定已持有 Cookie
  • 不把所有登录成功率押在 Cookie 域、SameSite、过期策略等细节上
  • 不改变开源版原始登录逻辑

4.2 不推荐的做法

以下做法不建议在私有版采用:

  • 在全局 servlet filter 中对所有匿名请求自动尝试 SSO 登录
  • 直接在 controller、filter 或 provider 里手写 HttpSessionSecurityContext 逻辑
  • 把私有 SSO 的 UID 映射成临时整数 ID 再作为用户主标识
  • 按 email 自动合并账号
  • 让前端直接调用私有 SSO 的内部校验接口
  • 为私有版新增一整套与开源版平行的“私有登录 session 机制”

5. 私有版最小差异实施方案

5.1 后端应新增什么

私有仓库建议只新增以下实现类,不改主链路:

  1. 一个 DirectAuthProvider 实现
  2. 一个 PassiveSessionAuthenticator 实现
  3. 可选的 LogoutPropagationHandler 实现
  4. 私有配置属性类或私有配置项
  5. 若 SSO 返回的是外部 UID 而不是现成平台用户,需要补充“根据 SSO UID 查询或创建平台用户”的私有服务

建议命名示例:

  • PrivateSsoDirectAuthProvider
  • PrivateSsoPassiveSessionAuthenticator
  • PrivateSsoLogoutPropagationHandler
  • PrivateSsoProperties
  • PrivateSsoIdentityService

不建议修改这些公共类的职责:

  • PlatformSessionService
  • LocalAuthController
  • AuthController
  • SecurityConfig

5.2 后端建议实现步骤

步骤 1定义 provider code

私有版统一使用稳定 provider code

private-sso

要求:

  • DirectAuthProvider.providerCode()PassiveSessionAuthenticator.providerCode() 返回同一个值
  • 不要为“用户名密码登录”和“Cookie 登录”定义两个不同 provider code
  • 如需更友好的登录页文案,请同时覆盖 provider 的 displayName(),避免前端再维护一份私有显示名映射

步骤 2封装 SSO 客户端

不要在 provider 实现里直接散落 HTTP 或 RPC 调用。建议先抽一层私有客户端:

public interface PrivateSsoClient {
    PrivateSsoUser verifyPassword(String username, String password);
    Optional<PrivateSsoUser> verifySession(HttpServletRequest request);
}

其中 PrivateSsoUser 至少应包含:

  • uid
  • username
  • displayName

最佳实践:

  • 所有超时、重试、日志脱敏、错误码翻译都放在客户端层
  • provider 层只负责把外部结果映射成平台所需的身份对象
  • 禁止记录明文密码

步骤 3实现用户映射服务

私有 SSO 不依赖 email也不做账号合并因此建议私有版实现一个专用服务

public interface PrivateSsoIdentityService {
    PlatformPrincipal resolveOrCreate(PrivateSsoUser ssoUser);
}

推荐逻辑:

  1. providerCode=private-ssosubject=ssoUid 查现有绑定
  2. 若已存在,加载对应平台用户
  3. 若不存在,则自动创建平台用户
  4. 创建新的身份绑定
  5. 返回 PlatformPrincipal

要求:

  • 自动创建出的用户默认应是 ACTIVE
  • 不要尝试和现有本地账号或 OAuth 账号按 email 合并

步骤 4实现 DirectAuthProvider

伪代码如下:

@Component
public class PrivateSsoDirectAuthProvider implements DirectAuthProvider {

    @Override
    public String providerCode() {
        return "private-sso";
    }

    @Override
    public PlatformPrincipal authenticate(DirectAuthRequest request) {
        PrivateSsoUser ssoUser = privateSsoClient.verifyPassword(
            request.username(),
            request.password()
        );
        return privateSsoIdentityService.resolveOrCreate(ssoUser);
    }
}

要求:

  • 只返回认证成功后的 PlatformPrincipal
  • 不在这里建立 Session
  • 不在这里写 SecurityContext

步骤 5实现 PassiveSessionAuthenticator

伪代码如下:

@Component
public class PrivateSsoPassiveSessionAuthenticator implements PassiveSessionAuthenticator {

    @Override
    public String providerCode() {
        return "private-sso";
    }

    @Override
    public Optional<PlatformPrincipal> authenticate(HttpServletRequest request) {
        return privateSsoClient.verifySession(request)
            .map(privateSsoIdentityService::resolveOrCreate);
    }
}

要求:

  • 只消费当前请求已带上的 Cookie 或其他被动凭证
  • 不主动重定向到 SSO
  • 不在这里自行创建 Session

步骤 6开启配置

私有版部署时启用:

skillhub:
  auth:
    direct:
      enabled: true
    session-bootstrap:
      enabled: true

建议:

  • 预发环境先只开 direct auth
  • passive bootstrap 在确认 Cookie 域和 SameSite 行为可靠后再开启

6. 前端最佳实践

6.1 推荐的登录页策略

私有版推荐保留当前开源登录页结构,但增加企业 SSO 入口:

  • 保留 OAuth 按钮
  • 本地账号登录是否保留,由私有版自行决定
  • 增加企业 SSO 用户名密码表单,或将现有密码表单切换到 direct auth 兼容接口
  • 增加“从企业 SSO 登录”按钮,对应 session/bootstrap

推荐优先级:

  1. 首先提供明确可见的企业用户名密码登录
  2. 其次提供“从企业 SSO 登录”按钮
  3. 最后才考虑自动 bootstrap

6.2 自动 bootstrap 的使用建议

只有在以下条件同时满足时才建议开启 SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO=true

  • 已确认浏览器在 skill.xxx.com 下能稳定带上 SSO Cookie
  • 失败时 UI 不会卡死或重复重试
  • 页面只会自动尝试一次
  • 前端不会因为自动尝试失败而阻断正常密码登录

如果以上条件不满足,建议只显示一个显式按钮,让用户主动触发。

6.3 前端禁止事项

  • 不要把密码提交给非 SkillHub 后端地址
  • 不要在浏览器里解析或操作私有 SSO 内部 Cookie 细节
  • 不要把 bootstrap 失败当成页面级致命错误

7. Spring Session Redis 相关约束

当前平台的统一 Web 登录态是 Spring Session。

后续私有版继续接入时,必须遵守以下规则:

  • 所有成功登录都必须通过 PlatformSessionService
  • 所有 Web 会话都通过 HttpSession 持久化
  • 不要手动维护第二份“私有 SSO session”
  • 不要在 Redis 中自行定义另一套认证缓存结构来替代 Session

当前统一服务会做的事:

  • 写入 platformPrincipal
  • 写入 SPRING_SECURITY_CONTEXT
  • 在交互式登录流程中轮换 session id

8. 安全最佳实践

8.1 用户名密码直连场景

  • SkillHub 后端与私有 SSO 之间必须走内网或可信 RPC
  • 明文密码只允许存在于浏览器提交和后端调用 SSO 的瞬时链路中
  • 日志、埋点、异常信息中禁止出现密码
  • 对下游 SSO 调用应设置超时和熔断策略
  • 必须先确认 Cookie 域、路径、SameSite、Secure 策略能满足 skill.xxx.com 使用
  • bootstrap 接口应保留 CSRF 防护
  • 失败时只返回认证失败,不泄露过多 Cookie 校验细节
  • 除非有明确产品要求,否则不要做无感知的全站自动登录 filter

8.3 身份映射场景

  • 只信任稳定 UID不信任显示名作为主身份依据
  • 不按 email 合并
  • 不按 username 合并

9. 建议测试矩阵

9.1 后端单元测试

  • DirectAuthProvider 成功认证
  • DirectAuthProvider 认证失败
  • PassiveSessionAuthenticator 在有效 Cookie 下成功返回主体
  • PassiveSessionAuthenticator 在无效 Cookie 下返回空或失败
  • PrivateSsoIdentityService 首次登录自动建号
  • PrivateSsoIdentityService 再次登录复用已有绑定

9.2 后端集成测试

  • POST /api/v1/auth/direct/login 在开启配置后能建立 Session
  • POST /api/v1/auth/session/bootstrap 在开启配置后能建立 Session
  • 成功登录后 /api/v1/auth/me 返回正确用户
  • direct auth 与现有 /api/v1/auth/local/login 不互相影响
  • bootstrap 关闭时仍返回 403
  • direct auth 关闭时仍返回 403

9.3 前端测试

  • 未开启运行时开关时,登录页与开源版默认行为一致
  • 开启 direct auth 后,密码表单请求走 /api/v1/auth/direct/login
  • 开启 bootstrap 按钮后,点击能触发 bootstrap 请求
  • 自动 bootstrap 失败后,用户仍可正常使用其它登录入口

9.4 手工验收

  • 已登录 SSO 的浏览器中bootstrap 能成功建立 SkillHub 登录态
  • 未登录 SSO 的浏览器中bootstrap 失败但不影响密码登录
  • direct auth 登录成功后,刷新页面仍保持登录态
  • 多 Pod 环境下,借助 Spring Session Redis切换实例后 session 仍有效

10. 推荐开发顺序

如果后续在私有仓库中真正开始接入,建议按下面顺序推进:

  1. 实现 PrivateSsoClient
  2. 实现 PrivateSsoIdentityService
  3. 实现 PrivateSsoDirectAuthProvider
  4. 先启用 skillhub.auth.direct.enabled=true
  5. 前端接通 direct auth 入口并完成测试
  6. 再实现 PrivateSsoPassiveSessionAuthenticator
  7. 确认 Cookie 作用域和浏览器行为
  8. 启用 session-bootstrap
  9. 视需要决定是否开启自动 bootstrap

11. 给 coding agent 的执行指令

如果后续由 AI 继续在私有仓库上完成接入,建议严格遵守以下执行规则:

  • 先读 11-auth-extensibility-and-private-sso.md 和本文档
  • 不要重构现有公共认证主链路,除非发现明确 bug
  • 私有 SSO 的具体实现优先写成 provider、authenticator、client、identity service
  • 不要复制 PlatformSessionService 逻辑
  • 不要在多个 controller 或 filter 中重复写 Session 建立代码
  • 任何新增前端行为都必须保证运行时配置关闭时完全不影响开源版
  • 所有新增协议和运行时配置必须同步更新文档
  • 每完成一个阶段都跑后端测试;涉及前端改动时再补跑 pnpm typecheckpnpm build

12. 完成定义

当私有版 SSO 接入完成时,应满足以下标准:

  • 开源版默认登录方式仍然不变
  • 私有版只通过扩展点接入,没有复制一套独立登录架构
  • direct auth 可用
  • session bootstrap 可用
  • 首次 SSO 登录自动建号
  • 统一使用 Spring Session Redis 承载 Web 登录态
  • /api/v1/auth/me、RBAC、现有业务接口对登录来源无感知
  • 文档、配置、测试都完整