mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
438 lines
15 KiB
Markdown
438 lines
15 KiB
Markdown
# 私有 SSO 接入兼容层实施手册
|
||
|
||
## 1. 文档目的
|
||
|
||
本文档面向两类读者:
|
||
|
||
- 后续在私有仓库中接入企业 SSO 的开发者
|
||
- 需要基于当前开源版兼容层继续开发的 coding agent
|
||
|
||
本文档不是认证架构总览,而是实施手册。目标是让后续执行者在不了解全部历史上下文的情况下,也能基于当前成果直接开始接入工作,并且尽量把私有仓库与开源仓库的差异控制在 provider 实现层和少量配置层。
|
||
|
||
相关文档:
|
||
|
||
- [03-authentication-design.md](/Users/xudongsun/github/skillhub/docs/03-authentication-design.md)
|
||
- [06-api-design.md](/Users/xudongsun/github/skillhub/docs/06-api-design.md)
|
||
- [08-frontend-architecture.md](/Users/xudongsun/github/skillhub/docs/08-frontend-architecture.md)
|
||
- [11-auth-extensibility-and-private-sso.md](/Users/xudongsun/github/skillhub/docs/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`
|
||
- 用于未来低优先级登出联动
|
||
|
||
关键代码位置:
|
||
|
||
- [DirectAuthProvider.java](/Users/xudongsun/github/skillhub/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/DirectAuthProvider.java)
|
||
- [PassiveSessionAuthenticator.java](/Users/xudongsun/github/skillhub/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/bootstrap/PassiveSessionAuthenticator.java)
|
||
- [PlatformSessionService.java](/Users/xudongsun/github/skillhub/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/PlatformSessionService.java)
|
||
|
||
### 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 里手写 `HttpSession` 和 `SecurityContext` 逻辑
|
||
- 把私有 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:
|
||
|
||
```text
|
||
private-sso
|
||
```
|
||
|
||
要求:
|
||
|
||
- `DirectAuthProvider.providerCode()` 和 `PassiveSessionAuthenticator.providerCode()` 返回同一个值
|
||
- 不要为“用户名密码登录”和“Cookie 登录”定义两个不同 provider code
|
||
- 如需更友好的登录页文案,请同时覆盖 provider 的 `displayName()`,避免前端再维护一份私有显示名映射
|
||
|
||
#### 步骤 2:封装 SSO 客户端
|
||
|
||
不要在 provider 实现里直接散落 HTTP 或 RPC 调用。建议先抽一层私有客户端:
|
||
|
||
```java
|
||
public interface PrivateSsoClient {
|
||
PrivateSsoUser verifyPassword(String username, String password);
|
||
Optional<PrivateSsoUser> verifySession(HttpServletRequest request);
|
||
}
|
||
```
|
||
|
||
其中 `PrivateSsoUser` 至少应包含:
|
||
|
||
- `uid`
|
||
- `username`
|
||
- `displayName`
|
||
|
||
最佳实践:
|
||
|
||
- 所有超时、重试、日志脱敏、错误码翻译都放在客户端层
|
||
- provider 层只负责把外部结果映射成平台所需的身份对象
|
||
- 禁止记录明文密码
|
||
|
||
#### 步骤 3:实现用户映射服务
|
||
|
||
私有 SSO 不依赖 email,也不做账号合并,因此建议私有版实现一个专用服务:
|
||
|
||
```java
|
||
public interface PrivateSsoIdentityService {
|
||
PlatformPrincipal resolveOrCreate(PrivateSsoUser ssoUser);
|
||
}
|
||
```
|
||
|
||
推荐逻辑:
|
||
|
||
1. 按 `providerCode=private-sso` 和 `subject=ssoUid` 查现有绑定
|
||
2. 若已存在,加载对应平台用户
|
||
3. 若不存在,则自动创建平台用户
|
||
4. 创建新的身份绑定
|
||
5. 返回 `PlatformPrincipal`
|
||
|
||
要求:
|
||
|
||
- 自动创建出的用户默认应是 `ACTIVE`
|
||
- 不要尝试和现有本地账号或 OAuth 账号按 email 合并
|
||
|
||
#### 步骤 4:实现 `DirectAuthProvider`
|
||
|
||
伪代码如下:
|
||
|
||
```java
|
||
@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`
|
||
|
||
伪代码如下:
|
||
|
||
```java
|
||
@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:开启配置
|
||
|
||
私有版部署时启用:
|
||
|
||
```yaml
|
||
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 调用应设置超时和熔断策略
|
||
|
||
### 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` 在开启配置后能建立 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](/Users/xudongsun/github/skillhub/docs/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、现有业务接口对登录来源无感知
|
||
- 文档、配置、测试都完整
|