15 KiB
私有 SSO 接入兼容层实施手册
1. 文档目的
本文档面向两类读者:
- 后续在私有仓库中接入企业 SSO 的开发者
- 需要基于当前开源版兼容层继续开发的 coding agent
本文档不是认证架构总览,而是实施手册。目标是让后续执行者在不了解全部历史上下文的情况下,也能基于当前成果直接开始接入工作,并且尽量把私有仓库与开源仓库的差异控制在 provider 实现层和少量配置层。
相关文档:
- 03-authentication-design.md
- 06-api-design.md
- 08-frontend-architecture.md
- 11-auth-extensibility-and-private-sso.md
2. 当前上下文与已确认约束
本轮改造的真实目标不是在开源版里实现私有 SSO,而是先把开源版前后端改造成一个稳定的兼容接入层。
已经确认的业务前提如下:
- 私有 SSO 能返回稳定且唯一的 UID
- 用户名密码校验接口与基于 Cookie 的会话校验接口都返回同一个 UID
- SkillHub 私有版与私有 SSO 会部署在统一主域下,例如
skill.xxx.com与sso.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/loginPOST /api/v1/auth/session/bootstrapGET /api/v1/auth/methods
这些协议的设计原则如下:
- 默认关闭
- 默认没有私有 SSO 实现
- 启用后由 provider 扩展驱动
- 成功后统一建立标准 Spring Security Session
- 不替换现有
/api/v1/auth/local/login - 不替换现有 OAuth 登录
3.3 前端兼容层
当前开源版前端已经支持通过运行时配置开启兼容入口:
SKILLHUB_WEB_AUTH_DIRECT_ENABLEDSKILLHUB_WEB_AUTH_DIRECT_PROVIDERSKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLEDSKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDERSKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO
前端设计原则如下:
- 默认不启用任何私有登录入口
- 开启后通过兼容层切换,不破坏现有登录页默认行为
- 优先走统一目录接口
/api/v1/auth/methods - 被动会话登录优先使用显式 bootstrap,而不是页面加载时偷偷尝试多次
4. 私有 SSO 的推荐接入方案
4.1 推荐总策略
最佳实践不是只选一种方式,而是同时支持两条链路:
-
主路径:
DirectAuthProvider- 登录页展示企业 SSO 用户名密码表单
- 后端通过内部接口或 RPC 调用私有 SSO 校验
- 校验成功后给用户建立 SkillHub Session
-
补充路径:
PassiveSessionAuthenticator- 当用户已经在 SSO 系统登录过,并且浏览器会自动带上共享 Cookie 时
- 登录页允许用户主动点击“从企业 SSO 登录”
- 或在非常谨慎的前提下自动尝试一次 bootstrap
这样做的理由:
- 覆盖“尚未登录 SSO”和“已登录 SSO”两种用户状态
- 不依赖浏览器一定已持有 Cookie
- 不把所有登录成功率押在 Cookie 域、SameSite、过期策略等细节上
- 不改变开源版原始登录逻辑
4.2 不推荐的做法
以下做法不建议在私有版采用:
- 在全局 servlet filter 中对所有匿名请求自动尝试 SSO 登录
- 直接在 controller、filter 或 provider 里手写
HttpSession和SecurityContext逻辑 - 把私有 SSO 的 UID 映射成临时整数 ID 再作为用户主标识
- 按 email 自动合并账号
- 让前端直接调用私有 SSO 的内部校验接口
- 为私有版新增一整套与开源版平行的“私有登录 session 机制”
5. 私有版最小差异实施方案
5.1 后端应新增什么
私有仓库建议只新增以下实现类,不改主链路:
- 一个
DirectAuthProvider实现 - 一个
PassiveSessionAuthenticator实现 - 可选的
LogoutPropagationHandler实现 - 私有配置属性类或私有配置项
- 若 SSO 返回的是外部 UID 而不是现成平台用户,需要补充“根据 SSO UID 查询或创建平台用户”的私有服务
建议命名示例:
PrivateSsoDirectAuthProviderPrivateSsoPassiveSessionAuthenticatorPrivateSsoLogoutPropagationHandlerPrivateSsoPropertiesPrivateSsoIdentityService
不建议修改这些公共类的职责:
PlatformSessionServiceLocalAuthControllerAuthControllerSecurityConfig
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 至少应包含:
uidusernamedisplayName
最佳实践:
- 所有超时、重试、日志脱敏、错误码翻译都放在客户端层
- provider 层只负责把外部结果映射成平台所需的身份对象
- 禁止记录明文密码
步骤 3:实现用户映射服务
私有 SSO 不依赖 email,也不做账号合并,因此建议私有版实现一个专用服务:
public interface PrivateSsoIdentityService {
PlatformPrincipal resolveOrCreate(PrivateSsoUser ssoUser);
}
推荐逻辑:
- 按
providerCode=private-sso和subject=ssoUid查现有绑定 - 若已存在,加载对应平台用户
- 若不存在,则自动创建平台用户
- 创建新的身份绑定
- 返回
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
推荐优先级:
- 首先提供明确可见的企业用户名密码登录
- 其次提供“从企业 SSO 登录”按钮
- 最后才考虑自动 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 调用应设置超时和熔断策略
8.2 Cookie 被动会话场景
- 必须先确认 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在开启配置后能建立 SessionPOST /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. 推荐开发顺序
如果后续在私有仓库中真正开始接入,建议按下面顺序推进:
- 实现
PrivateSsoClient - 实现
PrivateSsoIdentityService - 实现
PrivateSsoDirectAuthProvider - 先启用
skillhub.auth.direct.enabled=true - 前端接通 direct auth 入口并完成测试
- 再实现
PrivateSsoPassiveSessionAuthenticator - 确认 Cookie 作用域和浏览器行为
- 启用
session-bootstrap - 视需要决定是否开启自动 bootstrap
11. 给 coding agent 的执行指令
如果后续由 AI 继续在私有仓库上完成接入,建议严格遵守以下执行规则:
- 先读 11-auth-extensibility-and-private-sso.md 和本文档
- 不要重构现有公共认证主链路,除非发现明确 bug
- 私有 SSO 的具体实现优先写成 provider、authenticator、client、identity service
- 不要复制
PlatformSessionService逻辑 - 不要在多个 controller 或 filter 中重复写 Session 建立代码
- 任何新增前端行为都必须保证运行时配置关闭时完全不影响开源版
- 所有新增协议和运行时配置必须同步更新文档
- 每完成一个阶段都跑后端测试;涉及前端改动时再补跑
pnpm typecheck和pnpm build
12. 完成定义
当私有版 SSO 接入完成时,应满足以下标准:
- 开源版默认登录方式仍然不变
- 私有版只通过扩展点接入,没有复制一套独立登录架构
- direct auth 可用
- session bootstrap 可用
- 首次 SSO 登录自动建号
- 统一使用 Spring Session Redis 承载 Web 登录态
/api/v1/auth/me、RBAC、现有业务接口对登录来源无感知- 文档、配置、测试都完整