diff --git a/docs/sh-sso/README.md b/docs/sh-sso/README.md
new file mode 100644
index 00000000..d622589d
--- /dev/null
+++ b/docs/sh-sso/README.md
@@ -0,0 +1,313 @@
+# 企业 SSO 登录接入文档索引
+
+## 📚 文档概览
+
+本目录包含 SkillHub 平台接入企业 SSO 统一登录的完整设计文档和流程图。
+
+---
+
+## 📖 主要文档
+
+### 1. [企业 SSO 登录接入方案设计](./企业SSO登录接入方案设计.md)
+
+**主设计文档** - 全面的技术方案设计
+
+包含内容:
+- ✅ 方案概述与设计原则
+- ✅ 系统架构设计
+- ✅ 详细流程设计
+- ✅ 接口设计(前后端)
+- ✅ 数据模型设计
+- ✅ 安全设计
+- ✅ 实施步骤与时间规划
+- ✅ 配置管理
+- ✅ 监控告警
+- ✅ 风险评估与应对
+- ✅ FAQ
+
+**适合人群**: 项目负责人、架构师、技术经理
+
+---
+
+### 2. [企业 SSO 首次登录流程](./企业SSO首次登录流程.md)
+
+**首次登录详细流程** - 含 Mermaid 流程图
+
+关键点:
+- 🔐 SSO 认证流程
+- 🔑 授权码换 Token
+- 🔒 首次登录强制修改密码
+- 👤 平台账号创建与绑定
+- 💾 Token 缓存策略
+- ⚠️ 错误处理场景
+
+**适合人群**: 后端开发、前端开发、测试工程师
+
+---
+
+### 3. [企业 SSO 常规登录流程](./企业SSO常规登录流程.md)
+
+**常规登录详细流程** - 含 Mermaid 流程图
+
+关键点:
+- ⚡ SSO Session 免密登录
+- 🔄 Token 自动刷新机制
+- 🔃 用户信息同步策略
+- 📊 性能优化方案
+- 🛡️ 降级方案设计
+
+**适合人群**: 后端开发、前端开发、运维工程师
+
+---
+
+### 4. [企业 SSO 租户切换流程](./企业SSO租户切换流程.md)
+
+**租户切换详细流程** - 含 Mermaid 流程图
+
+关键点:
+- 🏢 多租户架构设计
+- 🔀 租户切换机制
+- 🗑️ 缓存清除策略
+- 🔐 数据隔离实现
+- 🎣 前端 Hook 实现
+- 🚫 权限验证与限流
+
+**适合人群**: 后端开发、前端开发、架构师
+
+---
+
+### 5. [企业 SSO 系统架构图](./企业SSO系统架构图.md)
+
+**系统架构详细设计** - 含多个 Mermaid 架构图
+
+包含架构图:
+- 📐 总体架构图
+- 🔐 认证流程架构
+- ♻️ Token 生命周期管理
+- 🗄️ 数据库 ER 图
+- 📦 Redis 缓存架构
+- 🏢 多租户数据隔离
+- 🛡️ 安全防护层次
+- 🚀 部署架构
+
+**适合人群**: 架构师、运维工程师、技术经理
+
+---
+
+### 6. [企业 SSO 接口文档](./sh-sso-借口设计文档.md)
+
+**企业 SSO 系统接口文档** - 标准 API 文档
+
+包含接口:
+1. 获取 SSO 登录地址
+2. 登录提交接口
+3. 修改密码接口
+4. 退出登录
+5. 获取用户信息
+6. 刷新用户 Token
+
+**适合人群**: 后端开发、接口对接人员
+
+---
+
+## 🗺️ 阅读路径建议
+
+### 对于项目负责人/架构师
+
+```
+1. 企业SSO登录接入方案设计.md (全面了解方案)
+ ↓
+2. 企业SSO系统架构图.md (理解技术架构)
+ ↓
+3. 各流程文档 (了解实施细节)
+```
+
+### 对于后端开发
+
+```
+1. 企业SSO登录接入方案设计.md (第4节:接口设计)
+ ↓
+2. 企业SSO首次登录流程.md (实现首次登录)
+ ↓
+3. 企业SSO常规登录流程.md (实现常规登录)
+ ↓
+4. 企业SSO租户切换流程.md (实现租户切换)
+ ↓
+5. sh-sso-借口设计文档.md (对接企业SSO API)
+```
+
+### 对于前端开发
+
+```
+1. 企业SSO登录接入方案设计.md (第4.2节:前端接口)
+ ↓
+2. 企业SSO首次登录流程.md (实现登录页面)
+ ↓
+3. 企业SSO租户切换流程.md (实现租户切换组件)
+```
+
+### 对于测试工程师
+
+```
+1. 企业SSO登录接入方案设计.md (了解整体方案)
+ ↓
+2. 各流程文档的"测试用例"章节 (编写测试用例)
+ ↓
+3. 各流程文档的"错误处理"章节 (异常场景测试)
+```
+
+---
+
+## 🔍 快速查找
+
+### 按功能查找
+
+| 功能 | 文档位置 |
+|------|----------|
+| 如何实现首次登录? | [企业SSO首次登录流程.md](./企业SSO首次登录流程.md) |
+| 如何实现免密登录? | [企业SSO常规登录流程.md](./企业SSO常规登录流程.md#sso-session-免密登录) |
+| 如何实现租户切换? | [企业SSO租户切换流程.md](./企业SSO租户切换流程.md) |
+| Token 如何刷新? | [企业SSO常规登录流程.md](./企业SSO常规登录流程.md#token-自动刷新机制) |
+| 如何保证数据隔离? | [企业SSO系统架构图.md](./企业SSO系统架构图.md#多租户数据隔离) |
+| 如何配置 Redis? | [企业SSO登录接入方案设计.md](./企业SSO登录接入方案设计.md#81-后端配置applicationyml) |
+| 有哪些安全措施? | [企业SSO登录接入方案设计.md](./企业SSO登录接入方案设计.md#6-安全设计) |
+
+### 按角色查找
+
+| 角色 | 关注重点 | 文档推荐 |
+|------|----------|----------|
+| 产品经理 | 用户体验、功能流程 | 各流程文档 |
+| 后端开发 | 接口实现、数据库设计 | 方案设计 + 流程文档 + 接口文档 |
+| 前端开发 | 组件实现、状态管理 | 流程文档(前端实现章节) |
+| 测试工程师 | 测试用例、异常场景 | 流程文档(测试用例章节) |
+| 运维工程师 | 部署架构、监控告警 | 系统架构图 + 方案设计 |
+| 安全工程师 | 安全防护、审计日志 | 方案设计(安全设计) |
+
+---
+
+## 📊 实施进度跟踪
+
+### 阶段一:基础框架搭建(预计 1-2 天)
+
+- [ ] 创建 `EnterpriseDirectAuthProvider`
+- [ ] 创建 `EnterpriseSsoClient`
+- [ ] 配置 Spring Security
+- [ ] 创建前端登录入口
+
+### 阶段二:核心流程实现(预计 3-4 天)
+
+- [ ] 实现获取 SSO 登录地址接口
+- [ ] 实现 SSO 回调处理
+- [ ] 实现用户信息同步
+- [ ] 实现 Token 刷新机制
+- [ ] 前端登录流程实现
+
+### 阶段三:租户管理功能(预计 2-3 天)
+
+- [ ] 实现租户切换接口
+- [ ] 实现数据隔离
+- [ ] 前端租户切换组件
+
+### 阶段四:安全加固和测试(预计 2-3 天)
+
+- [ ] Token 加密存储
+- [ ] CSRF 防护配置
+- [ ] 单元测试(覆盖率 80%+)
+- [ ] 集成测试
+- [ ] E2E 测试
+
+### 阶段五:文档和部署(预计 1 天)
+
+- [ ] 编写运维文档
+- [ ] 配置生产环境
+- [ ] 灰度发布
+- [ ] 监控配置
+
+---
+
+## 🔧 技术栈
+
+### 后端
+
+- Spring Boot 3.x
+- Spring Security 6.x
+- MyBatis / JPA
+- Redis (Lettuce)
+- MySQL 8.0
+
+### 前端
+
+- React 18
+- TypeScript
+- TanStack Query (React Query)
+- Axios
+- Ant Design / Material-UI
+
+### 基础设施
+
+- Nginx / ALB
+- Redis Sentinel
+- MySQL 主从复制
+- Prometheus + Grafana
+
+---
+
+## 📞 联系方式
+
+### 技术支持
+
+- **项目负责人**: [待补充]
+- **架构师**: [待补充]
+- **后端负责人**: [待补充]
+- **前端负责人**: [待补充]
+
+### 问题反馈
+
+- **技术问题**: 提交 Issue 到项目仓库
+- **需求变更**: 联系产品经理
+- **紧急问题**: [待补充联系方式]
+
+---
+
+## 📝 更新日志
+
+| 版本 | 日期 | 更新内容 | 作者 |
+|------|------|----------|------|
+| v1.0 | 2026-04-03 | 初始版本,完成所有设计文档 | Claude |
+
+---
+
+## 📌 附录
+
+### 相关资源
+
+- [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
+- [Spring Security OAuth2](https://spring.io/projects/spring-security-oauth)
+- [React Query 文档](https://tanstack.com/query/latest)
+
+### 术语表
+
+| 术语 | 说明 |
+|------|------|
+| SSO | Single Sign-On,单点登录 |
+| Access Token | 访问令牌,用于 API 访问授权 |
+| Refresh Token | 刷新令牌,用于获取新的 Access Token |
+| RBAC | Role-Based Access Control,基于角色的访问控制 |
+| CSRF | Cross-Site Request Forgery,跨站请求伪造 |
+| XSS | Cross-Site Scripting,跨站脚本攻击 |
+| JWT | JSON Web Token |
+| HTTPS | HTTP Secure,安全的 HTTP 协议 |
+
+---
+
+**文档完整性检查清单**
+
+- [x] 主设计文档
+- [x] 首次登录流程
+- [x] 常规登录流程
+- [x] 租户切换流程
+- [x] 系统架构图
+- [x] 接口文档
+- [x] README 索引
+
+所有文档已完成!✅
diff --git a/docs/sh-sso/sh-sso-借口设计文档.md b/docs/sh-sso/sh-sso-借口设计文档.md
new file mode 100644
index 00000000..985a4b69
--- /dev/null
+++ b/docs/sh-sso/sh-sso-借口设计文档.md
@@ -0,0 +1,415 @@
+# SSO 接口设计文档
+
+## 1. 获取 SSO 登录地址
+
+### 接口描述
+
+返回 SSO 登录地址。
+
+### 请求信息
+
+- **请求方法**: `GET`
+- **请求 URL**: `/auth/login-url`
+
+### 请求参数
+
+| 参数名 | 位置 | 类型 | 是否必填 | 描述 |
+|--------|------|------|----------|------|
+| `redirectUri` | Query | String | 是 | 登录成功后的回跳地址 |
+
+### 请求头
+
+| 参数名 | 类型 | 是否必填 | 描述 |
+|--------|------|----------|------|
+| `Authorization` | String | 否 | Bearer Token(认证信息) |
+
+### 响应示例
+
+**成功响应**
+
+```json
+{
+ "data": "http://localhost:8086/sso/login?clientId=1000&redirectUri=www.baidu.com",
+ "code": 1,
+ "message": "成功"
+}
+```
+
+**错误响应**
+
+```json
+{
+ "code": 9998,
+ "message": "Required request parameter 'redirectUri' for method parameter type String is not present",
+ "data": null
+}
+```
+
+---
+## 2. 登录提交接口
+
+### 接口描述
+
+创建用户在前端页面输入用户名和密码后,登录提交接口。
+
+### 请求信息
+
+- **请求方法**: `POST`
+- **请求 URL**: `http://api-dev.rainbowlab.net/sso/auth/sso/login`
+
+### 请求参数(Body)
+
+```json
+{
+ "clientId": "1000", // 必填
+ "username": "zhangbo", // 必填
+ "password": "123456" // 必填
+}
+```
+
+### 请求头
+
+| 参数名 | 类型 | 是否必填 | 描述 |
+|--------|------|----------|------|
+| `Content-Type` | String | 是 | application/json |
+
+### 响应示例
+
+**成功响应**
+
+```json
+{
+ "data": {
+ "accessToken": "AT-387dd3c28d0f4f359b9c6a5778536560",
+ "refreshToken": "RT-5f491a9bb6334516b8bd2e87b268d079",
+ "expiresIn": 1800,
+ "refreshExpiresIn": 7200,
+ "loginUserVO": {
+ "id": 1,
+ "username": "zhangbo",
+ "firstLogin": false
+ }
+ },
+ "code": 200,
+ "message": "成功"
+}
+```
+
+**错误响应**
+
+```json
+{
+ "data": null,
+ "code": 401,
+ "message": "用户不存在或密码不正确!"
+}
+```
+
+---
+## 3. 修改密码接口
+
+### 接口描述
+
+第一次登录成功后,修改密码不退出登录;登录成功后的首页的修改密码,修改完要退出登录重新登录。
+
+### 请求信息
+
+- **请求方法**: `POST`
+- **请求 URL**: `http://api-dev.rainbowlab.net/sso/password/save-password`
+
+### 请求参数(Body)
+
+```json
+{
+ "password": "newPassword", // 必填
+ "Logoutflag": "logout" // 必填: logout(退出登录), no_logout(不退出登录)
+}
+```
+
+### 请求头
+
+| 参数名 | 类型 | 是否必填 | 描述 |
+|--------|------|----------|------|
+| `Content-Type` | String | 是 | application/json |
+| `authorization` | String | 是 | SSO 的 access-token |
+
+### 响应示例
+
+**成功响应**
+
+```json
+{
+ "data": null,
+ "code": 200,
+ "message": "成功"
+}
+```
+
+**错误响应**
+
+```json
+{
+ "data": null,
+ "code": 406,
+ "message": "密码必须包含大小写字母和数字,长度8-20位!"
+}
+```
+
+```json
+{
+ "data": null,
+ "code": 407,
+ "message": "新密码与旧密码一致,请重新设置!"
+}
+```
+
+---
+## 4. 退出登录
+
+### 接口描述
+
+退出登录接口。
+
+### 请求信息
+
+- **请求方法**: `GET`
+- **请求 URL**: `http://api-dev.rainbowlab.net/sso/logout`
+
+### 请求参数
+
+无
+
+### 请求头
+
+| 参数名 | 类型 | 是否必填 | 描述 |
+|--------|------|----------|------|
+| `Content-Type` | String | 是 | application/json |
+| `authorization` | String | 是 | SSO 的 access-token |
+
+### 响应示例
+
+**成功响应**
+
+```json
+{
+ "data": "SUCCESS",
+ "code": 200,
+ "message": "成功"
+}
+```
+
+**错误响应**
+
+```json
+{
+ "data": null,
+ "code": 415,
+ "message": "缺少token"
+}
+```
+
+---
+## 5. 获取用户信息
+
+> **更新日志**: 2026年4月1日迭代(增加部门任职信息)
+
+### 接口描述
+
+获取用户详情信息。
+
+### 请求信息
+
+- **请求方法**: `GET`
+- **请求 URL**: `http://api-dev.rainbowlab.net/sso/userinfo`
+
+### 请求参数
+
+无
+
+### 请求头
+
+| 参数名 | 类型 | 是否必填 | 描述 |
+|--------|------|----------|------|
+| `Content-Type` | String | 是 | application/json |
+| `authorization` | String | 是 | SSO 的 access-token |
+
+### 响应示例
+
+**成功响应**
+
+```json
+{
+ "data": {
+ "id": 53,
+ "employeeId": 74,
+ "nickName": "张波1",
+ "username": "张波",
+ "phone": "18217371537",
+ "tenantNo": "8000",
+ "tenantName": "盛虹石化",
+ "roleCode": "EMPLOYEE",
+ "roleName": "普通成员",
+ "positionCode": null,
+ "positionName": null,
+ "tenantList": [
+ {
+ "employeeId": 66,
+ "empName": "用户-QGJNDWWD",
+ "position": null,
+ "tenantNo": "6000",
+ "tenantName": "江苏东方盛虹股份有限公司"
+ },
+ {
+ "employeeId": 74,
+ "empName": "张波",
+ "position": null,
+ "tenantNo": "8000",
+ "tenantName": "盛虹石化"
+ }
+ ],
+ "employments": [
+ {
+ "employeeId": 74,
+ "orgCodePath": "/60000000/66000078/66000022/",
+ "orgCodePathName": "/石化板块/原油供应链中心/盛虹新加坡/",
+ "positionCode": "63002474",
+ "positionName": "综合副经理"
+ },
+ {
+ "employeeId": 74,
+ "orgCodePath": "/60000000/",
+ "orgCodePathName": "/石化板块/",
+ "positionCode": "63002209",
+ "positionName": "成品油部长"
+ }
+ ]
+ },
+ "code": 200,
+ "message": "成功"
+}
+```
+
+**错误响应**
+
+```json
+{
+ "data": null,
+ "code": 415,
+ "message": "缺少token"
+}
+```
+
+---
+## 6. 刷新用户 Token
+
+### 接口描述
+
+用户切换租户时,需要刷新 token,把切换的目标租户写入新 token。
+
+### 请求信息
+
+- **请求方法**: `POST`
+- **请求 URL**: `http://api-dev.rainbowlab.net/sso/auth/sso/refresh-token`
+
+### 请求参数(Body)
+
+```json
+{
+ "targentTenantNo": "6000",
+ "refreshToken": "RT-748b6c7d585543aeb2bc6456c79a64c1"
+}
+```
+
+### 请求头
+
+| 参数名 | 类型 | 是否必填 | 描述 |
+|--------|------|----------|------|
+| `Content-Type` | String | 是 | application/json |
+| `authorization` | String | 是 | SSO 的 access-token |
+
+### 响应示例
+
+**成功响应**
+
+```json
+{
+ "data": {
+ "accessToken": "AT-378c957bc4134112b78e49ce12fbba0b",
+ "refreshToken": "RT-5691c7d305eb4f039c1beb736a9c29dc",
+ "expiresIn": 36000,
+ "refreshExpiresIn": 72000,
+ "loginUserVO": {
+ "id": 3,
+ "username": "18217371537",
+ "firstLogin": false
+ }
+ },
+ "code": 200,
+ "message": "成功"
+}
+```
+
+**错误响应**
+
+```json
+{
+ "data": null,
+ "code": 415,
+ "message": "缺少token"
+}
+```
+
+---
+## 7. 错误码说明
+
+| 错误码 | 描述 |
+|--------|------|
+| 200 | 操作成功 |
+| 201 | 资源创建成功 |
+| 400 | 请求参数校验失败 |
+| 401 | 用户不存在或密码不正确 |
+| 403 | 无权限访问 |
+| 404 | 资源不存在 |
+| 406 | 密码必须包含大小写字母和数字,长度8-20位! |
+| 407 | 新密码与旧密码一致,请重新设置! |
+| 415 | 缺少 token |
+| 500 | 服务器内部错误 |
+
+---
+## 8. 测试示例(curl)
+
+### GET 请求示例
+
+```bash
+curl -X GET \
+ 'http://api.example.com/api/users/12345?fields=name,email' \
+ -H 'Authorization: Bearer your_token'
+```
+
+### POST 请求示例
+
+```bash
+curl -X POST \
+ http://api.example.com/api/users \
+ -H 'Content-Type: application/json' \
+ -H 'Authorization: Bearer your_token' \
+ -d '{
+ "name": "王五",
+ "email": "wangwu@example.com"
+ }'
+```
+
+---
+## 9. 其他说明
+
+### 分页支持
+
+对于列表接口(如 `/api/users`),可添加 `page` 和 `pageSize` 查询参数。
+
+### 安全性
+
+所有接口均需通过 Token 认证,Token 有效期为 2 小时。
+
+### 版本控制
+
+当前版本为 v1,URL 路径为 `/api/v1/...`。
\ No newline at end of file
diff --git a/docs/sh-sso/企业SSO常规登录流程.md b/docs/sh-sso/企业SSO常规登录流程.md
new file mode 100644
index 00000000..6c792bec
--- /dev/null
+++ b/docs/sh-sso/企业SSO常规登录流程.md
@@ -0,0 +1,330 @@
+# 企业 SSO 常规登录流程
+
+## 流程图
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant User as 用户浏览器
+ participant Frontend as SkillHub 前端
+ participant Backend as SkillHub 后端
+ participant SSO as 企业 SSO 系统
+ participant Redis as Redis 缓存
+ participant DB as 数据库
+
+ User->>Frontend: 访问登录页
+ Frontend->>User: 展示登录页面
+
+ User->>Frontend: 点击"企业账号登录"
+ Frontend->>Backend: GET /api/auth/sso/login-url?redirectUri={callback}
+
+ Backend->>Backend: 生成 state 参数
+ Backend->>Redis: 缓存 state (TTL: 5分钟)
+ Backend-->>Frontend: 返回 SSO 登录地址
+
+ Frontend->>User: 重定向到企业 SSO
+ Note over User,SSO: 跳转到企业 SSO 认证系统
+
+ User->>SSO: 访问 SSO 登录页
+
+ alt 用户已在 SSO 系统登录(SSO Session 有效)
+ Note over SSO: 检测到有效 SSO Session
+ SSO->>SSO: 直接生成授权码(无需输入密码)
+ SSO->>User: 立即重定向回 SkillHub
+ else 用户未登录或 Session 过期
+ SSO->>User: 展示登录表单
+ User->>SSO: 输入企业账号和密码
+ SSO->>SSO: 验证用户凭证
+
+ alt 认证失败
+ SSO->>User: 显示错误信息
+ User->>SSO: 重新输入
+ end
+
+ SSO->>SSO: 认证成功,生成授权码
+ SSO->>User: 重定向回 SkillHub
+ end
+
+ User->>Frontend: 访问 /auth/sso/callback?code=xxx&state=yyy
+ Frontend->>Backend: POST /api/auth/sso/callback
{code, state}
+
+ Backend->>Redis: 验证 state 参数
+ alt state 无效
+ Backend-->>Frontend: 返回 CSRF 错误
+ Frontend->>User: 提示重新登录
+ end
+
+ Backend->>SSO: POST /auth/sso/login
使用 code 换取 Token
+ SSO-->>Backend: 返回 Access Token + Refresh Token
+
+ Backend->>Backend: 解析 Token 中的用户 ID
+ Backend->>DB: 查询身份绑定记录
WHERE enterprise_user_id = ?
+
+ alt 绑定记录不存在
+ Note over Backend: 用户从未登录过,应走首次登录流程
+ Backend-->>Frontend: 返回错误(未绑定)
+ Frontend->>User: 提示联系管理员
+ end
+
+ Backend->>SSO: GET /userinfo
Header: Authorization: Bearer {token}
+ SSO-->>Backend: 返回最新用户信息
+
+ Backend->>DB: 更新用户信息
UPDATE users SET
nick_name=?, phone=?, updated_at=NOW()
+ Backend->>DB: 更新身份绑定信息
UPDATE enterprise_identity_binding
SET tenant_no=?, updated_at=NOW()
+
+ Backend->>Redis: 缓存 Access Token
Key: sso:access_token:{userId}
TTL: 1800 (30分钟)
+ Backend->>Redis: 缓存 Refresh Token
Key: sso:refresh_token:{userId}
TTL: 7200 (2小时)
+ Backend->>Redis: 缓存用户会话
Key: session:{sessionId}
TTL: 7200
+
+ Backend->>Backend: 生成 SkillHub JWT Token
+ Backend-->>Frontend: 返回登录成功
{user, token, requirePasswordChange: false}
+
+ Frontend->>Frontend: 保存 Token 到内存
+ Frontend->>Frontend: 更新全局认证状态
+
+ alt 有 returnTo 参数
+ Frontend->>User: 跳转到指定页面
+ else 无 returnTo 参数
+ Frontend->>User: 跳转到首页
+ end
+
+ User->>Frontend: 访问目标页面
+ Frontend->>Backend: API 请求
Header: Authorization: Bearer {token}
+
+ Backend->>Redis: 验证 Token 有效性
+ alt Token 即将过期(剩余时间 < 5分钟)
+ Backend->>Backend: 触发自动刷新机制
+ Backend->>SSO: POST /auth/sso/refresh-token
{refreshToken}
+ SSO-->>Backend: 返回新的 Access Token
+ Backend->>Redis: 更新缓存的 Token
+ Backend-->>Frontend: 返回数据 + 新 Token
+ Frontend->>Frontend: 更新内存中的 Token
+ else Token 有效
+ Backend->>DB: 查询数据
+ Backend-->>Frontend: 返回数据
+ end
+
+ Frontend->>User: 展示页面内容
+```
+
+## 常规登录 vs 首次登录对比
+
+| 特性 | 首次登录 | 常规登录 |
+|------|----------|----------|
+| **SSO Session** | 不存在 | 可能存在(免密登录) |
+| **绑定记录** | 不存在,需创建 | 已存在,直接使用 |
+| **修改密码** | 必须修改 | 不需要 |
+| **账号创建** | 需要创建平台账号 | 已有账号,更新信息 |
+| **用户体验** | 需要额外操作 | 流畅快速 |
+
+## SSO Session 免密登录
+
+### 工作原理
+
+当用户在企业 SSO 系统已登录时:
+
+1. 用户访问 SkillHub 登录页
+2. 跳转到 SSO 登录页
+3. **SSO 检测到有效 Session**
+4. SSO 直接生成授权码,无需输入密码
+5. 自动重定向回 SkillHub
+6. 用户感知:几乎无感知,秒级完成登录
+
+### 优点
+
+- **用户体验佳**: 无需重复输入密码
+- **真正的单点登录**: 一次登录,多系统通用
+- **安全性高**: Session 有有效期控制
+
+### Session 过期处理
+
+```
+SSO Session 有效期: 8 小时(企业可配置)
+
+过期后:
+ 1. 用户访问 SkillHub
+ 2. 跳转到 SSO 登录页
+ 3. SSO 检测 Session 过期
+ 4. 要求用户重新输入密码
+ 5. 完成登录流程
+```
+
+## Token 自动刷新机制
+
+### 刷新时机
+
+- **方式一**: Access Token 过期前 5 分钟自动刷新
+- **方式二**: API 返回 401 时触发刷新
+
+### 刷新流程
+
+```mermaid
+sequenceDiagram
+ participant Frontend as 前端
+ participant Backend as 后端
+ participant SSO as SSO 系统
+ participant Redis as Redis
+
+ Frontend->>Backend: API 请求
+ Backend->>Redis: 检查 Token 有效期
+
+ alt Token 剩余时间 < 5分钟
+ Backend->>SSO: POST /auth/sso/refresh-token
{refreshToken}
+ SSO->>SSO: 验证 Refresh Token
+
+ alt Refresh Token 有效
+ SSO-->>Backend: 返回新 Access Token
+ Backend->>Redis: 更新缓存
sso:access_token:{userId}
+ Backend-->>Frontend: 返回数据 + 新 Token
+ Frontend->>Frontend: 更新 Token
+ else Refresh Token 过期
+ SSO-->>Backend: 返回 401
+ Backend-->>Frontend: 返回 401
+ Frontend->>Frontend: 清除认证状态
+ Frontend->>Frontend: 跳转到登录页
+ end
+ else Token 仍然有效
+ Backend-->>Frontend: 返回数据
+ end
+```
+
+### 前端拦截器实现
+
+```typescript
+// Axios 响应拦截器
+axios.interceptors.response.use(
+ (response) => {
+ // 检查响应头中是否有新 Token
+ const newToken = response.headers['x-new-access-token']
+ if (newToken) {
+ // 更新内存中的 Token
+ updateAccessToken(newToken)
+ }
+ return response
+ },
+ async (error) => {
+ if (error.response?.status === 401) {
+ // Token 过期,尝试刷新
+ try {
+ const newToken = await refreshAccessToken()
+ updateAccessToken(newToken)
+ // 重试原请求
+ return axios.request(error.config)
+ } catch (refreshError) {
+ // Refresh Token 也过期,跳转登录
+ redirectToLogin()
+ }
+ }
+ return Promise.reject(error)
+ }
+)
+```
+
+## 用户信息同步策略
+
+### 同步时机
+
+1. **每次登录**: 从 SSO 获取最新信息并更新
+2. **租户切换**: 切换租户时更新租户相关信息
+3. **定时同步**: 后台任务每天同步(可选)
+
+### 同步字段
+
+```sql
+UPDATE users SET
+ nick_name = ?, -- 昵称/姓名
+ phone = ?, -- 手机号
+ email = ?, -- 邮箱
+ updated_at = NOW()
+WHERE id = ?;
+
+UPDATE enterprise_identity_binding SET
+ tenant_no = ?, -- 当前租户
+ employee_id = ?, -- 员工 ID
+ updated_at = NOW()
+WHERE user_id = ?;
+```
+
+### 不同步的字段
+
+- `username`: 不变,作为唯一标识
+- `id`: 平台内部 ID,不变
+- `roles`: 平台角色,手动管理
+- `created_at`: 创建时间,不变
+
+## 错误处理
+
+### 常见错误
+
+| 错误码 | 场景 | 处理方式 |
+|--------|------|----------|
+| 401 | Token 过期 | 自动刷新,刷新失败则跳转登录 |
+| 403 | 账号被禁用 | 显示提示,联系管理员 |
+| 404 | 绑定记录不存在 | 引导重新登录或联系管理员 |
+| 500 | SSO 系统异常 | 显示错误,提供降级方案 |
+
+### 降级方案
+
+当企业 SSO 不可用时:
+
+```
+1. 检测 SSO 服务健康状态
+2. 超过 3 次连续失败,触发降级
+3. 临时启用本地管理员登录
+4. 发送告警通知运维团队
+5. 显示维护公告给用户
+```
+
+## 性能监控
+
+### 关键指标
+
+- **登录耗时**: 从点击登录到进入首页的总耗时
+ - 目标: < 3 秒
+ - 告警阈值: > 5 秒
+
+- **Token 刷新成功率**: 成功刷新次数 / 总刷新次数
+ - 目标: > 99%
+ - 告警阈值: < 95%
+
+- **SSO API 响应时间**: 调用 SSO 接口的平均耗时
+ - 目标: < 500ms
+ - 告警阈值: > 2s
+
+### 性能优化
+
+1. **Redis 缓存**: 缓存 Token 和用户信息
+2. **并发请求**: 用户信息查询和 Token 缓存并发执行
+3. **CDN 加速**: 前端资源使用 CDN
+4. **连接复用**: 复用 HTTP 连接减少握手时间
+
+## 测试用例
+
+### 正常流程
+
+- [x] 已登录用户访问 SkillHub(SSO Session 有效)
+- [x] 未登录用户访问(需输入密码)
+- [x] Token 自动刷新成功
+- [x] 用户信息同步成功
+- [x] 多标签页 Token 同步
+
+### 异常流程
+
+- [x] SSO Session 过期
+- [x] Refresh Token 过期
+- [x] 绑定记录被删除
+- [x] 账号被禁用
+- [x] SSO 系统不可用
+
+### 性能测试
+
+- [x] 100 并发用户同时登录
+- [x] 1000 并发 API 请求
+- [x] Token 刷新频繁场景
+
+---
+
+**相关文档**:
+- [企业 SSO 登录接入方案设计](./企业SSO登录接入方案设计.md)
+- [企业 SSO 首次登录流程](./企业SSO首次登录流程.md)
+- [企业 SSO 租户切换流程](./企业SSO租户切换流程.md)
diff --git a/docs/sh-sso/企业SSO登录接入方案设计.md b/docs/sh-sso/企业SSO登录接入方案设计.md
new file mode 100644
index 00000000..998d3a20
--- /dev/null
+++ b/docs/sh-sso/企业SSO登录接入方案设计.md
@@ -0,0 +1,830 @@
+# SkillHub 企业 SSO 登录接入方案设计
+
+## 文档信息
+
+- **版本**: v1.0
+- **创建日期**: 2026-04-03
+- **设计目标**: 将 SkillHub 平台改造为支持企业 SSO 统一登录,实现与企业认证系统的无缝集成
+
+---
+
+## 1. 方案概述
+
+### 1.1 背景
+
+SkillHub 当前支持以下认证方式:
+- 本地用户名/密码登录(LocalDirectAuthProvider)
+- OAuth2 第三方登录(GitHub 等)
+- API Token 认证
+
+为适配企业内部使用场景,需要接入企业 SSO 认证系统,实现:
+- **统一身份认证**: 员工使用企业账号登录
+- **多租户支持**: 支持不同租户(部门/子公司)的用户访问
+- **租户切换**: 用户可在多个租户间切换
+- **首次登录强制修改密码**: 安全合规要求
+
+### 1.2 设计原则
+
+1. **最小侵入**: 基于现有的 `DirectAuthProvider` 接口扩展,不破坏现有认证体系
+2. **安全第一**: 遵循 OAuth 2.0 标准,Token 安全存储和传输
+3. **可扩展性**: 支持未来对接其他企业 SSO 系统
+4. **用户体验**: 无缝的单点登录体验,最少的跳转次数
+
+### 1.3 技术选型
+
+- **认证协议**: 基于 OAuth 2.0 Authorization Code Flow
+- **Token 管理**: Access Token + Refresh Token 双 Token 机制
+- **会话管理**: Spring Security Session + Redis 分布式会话
+- **前端框架**: React + TanStack Query
+- **状态管理**: React Context + Hooks
+
+---
+
+## 2. 架构设计
+
+### 2.1 系统架构图
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 用户浏览器 │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ 前端应用 │◄────────►│ SSO 登录页 │ │
+│ │ (React) │ │ │ │
+│ └──────┬───────┘ └──────────────┘ │
+└─────────┼───────────────────────────────────────────────────────┘
+ │
+ │ HTTPS
+ │
+┌─────────▼───────────────────────────────────────────────────────┐
+│ SkillHub 后端服务 │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ Spring Security Filter Chain │ │
+│ │ ┌────────────┐ ┌──────────────┐ ┌────────────────┐ │ │
+│ │ │ SSO Auth │ │ Session Auth │ │ API Token Auth │ │ │
+│ │ │ Filter │ │ Filter │ │ Filter │ │ │
+│ │ └─────┬──────┘ └──────────────┘ └────────────────┘ │ │
+│ └────────┼─────────────────────────────────────────────────┘ │
+│ │ │
+│ ┌────────▼──────────────────────────────────────────────┐ │
+│ │ EnterpriseDirectAuthProvider │ │
+│ │ (实现 DirectAuthProvider 接口) │ │
+│ │ ┌──────────────────────────────────────────────┐ │ │
+│ │ │ - 调用企业 SSO API │ │ │
+│ │ │ - Token 验证与刷新 │ │ │
+│ │ │ - 用户信息同步 │ │ │
+│ │ │ - 租户信息管理 │ │ │
+│ │ └──────────────────────────────────────────────┘ │ │
+│ └────────┬─────────────────────────────────────────────────┘ │
+│ │ │
+│ ┌────────▼──────────────────────────────────────────────┐ │
+│ │ Identity Binding Service │ │
+│ │ - 企业账号与平台账号绑定 │ │
+│ │ - 用户信息同步与更新 │ │
+│ └───────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────────────────┐ │
+│ │ Redis 会话存储 │ │
+│ │ - Access Token 缓存 │ │
+│ │ - Refresh Token 缓存 │ │
+│ │ - 用户会话信息 │ │
+│ └──────────────────────────────────────────────────────┘ │
+└──────────────────┬───────────────────────────────────────────┘
+ │
+ │ HTTPS
+ │
+┌──────────────────▼───────────────────────────────────────────┐
+│ 企业 SSO 认证系统 │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ - 用户认证 API │ │
+│ │ - Token 签发与验证 │ │
+│ │ - 用户信息查询 │ │
+│ │ - 租户管理 │ │
+│ └─────────────────────────────────────────────────────┘ │
+└──────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 认证流程分层
+
+#### 第一层:前端认证入口
+
+- 登录页面展示企业 SSO 登录入口
+- 跳转到企业 SSO 登录页面(携带 redirectUri)
+- 接收 SSO 回调并处理
+
+#### 第二层:SkillHub 认证适配
+
+- 实现 `EnterpriseDirectAuthProvider`
+- 与企业 SSO API 交互
+- 用户信息映射与绑定
+- 租户信息管理
+
+#### 第三层:平台身份管理
+
+- 用户账号创建与绑定
+- 权限角色分配
+- 会话管理
+
+---
+
+## 3. 详细流程设计
+
+### 3.1 首次登录流程
+
+详见流程图:`企业SSO首次登录流程.md`
+
+**关键步骤**:
+
+1. 用户访问 SkillHub 登录页
+2. 点击"企业账号登录"按钮
+3. 前端调用 `/api/auth/sso/login-url?redirectUri={前端回调地址}`
+4. 后端返回企业 SSO 登录地址
+5. 前端重定向到企业 SSO 登录页
+6. 用户在 SSO 页面输入企业账号密码
+7. SSO 验证成功后回调 SkillHub(携带临时授权码)
+8. SkillHub 后端通过授权码换取 Access Token 和 Refresh Token
+9. 后端验证 Token 并获取用户信息
+10. 判断是否首次登录(firstLogin = true)
+11. 如果是首次登录,返回前端标记,提示修改密码
+12. 用户修改密码后完成首次登录
+13. 创建 SkillHub 平台账号并绑定企业账号
+14. 返回 JWT 或 Session Cookie
+15. 前端跳转到首页
+
+### 3.2 常规登录流程
+
+详见流程图:`企业SSO常规登录流程.md`
+
+**关键步骤**:
+
+1-10. 同首次登录流程
+11. 判断 firstLogin = false,直接返回 Token
+12. 查询已绑定的平台账号
+13. 更新用户信息(姓名、部门、职位等)
+14. 返回 JWT 或 Session Cookie
+15. 前端跳转到首页
+
+### 3.3 租户切换流程
+
+详见流程图:`企业SSO租户切换流程.md`
+
+**关键步骤**:
+
+1. 用户在平台选择切换租户
+2. 前端调用 `/api/auth/sso/switch-tenant`(携带目标租户号和 Refresh Token)
+3. 后端调用企业 SSO 刷新 Token API
+4. SSO 返回新的 Access Token(包含目标租户信息)
+5. 后端更新会话中的租户信息
+6. 返回新的 Token 和用户信息
+7. 前端更新全局状态并刷新页面
+
+### 3.4 Token 刷新流程
+
+**Access Token 过期处理**:
+
+```
+用户请求 API
+ ↓
+检测到 401 (Token 过期)
+ ↓
+前端拦截器自动调用 /api/auth/sso/refresh-token
+ ↓
+后端使用 Refresh Token 换取新的 Access Token
+ ↓
+返回新 Token 给前端
+ ↓
+前端重试原请求
+```
+
+### 3.5 退出登录流程
+
+**本地退出 vs 全局退出**:
+
+#### 本地退出(推荐)
+
+1. 前端调用 `/api/auth/logout`
+2. 后端清除 SkillHub 会话
+3. 前端清除本地 Token
+4. 跳转到登录页
+
+**优点**: 不影响用户在其他企业系统的登录状态
+
+#### 全局退出(可选)
+
+1. 前端调用 `/api/auth/sso/logout`
+2. 后端调用企业 SSO 退出 API
+3. 清除 SkillHub 会话和企业 SSO 会话
+4. 跳转到登录页
+
+**优点**: 完全注销,安全性更高
+
+---
+
+## 4. 接口设计
+
+### 4.1 后端接口
+
+#### 4.1.1 获取 SSO 登录地址
+
+```http
+GET /api/auth/sso/login-url
+```
+
+**Query 参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| `redirectUri` | String | 是 | 登录成功后的前端回调地址 |
+
+**响应**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "loginUrl": "http://sso.company.com/login?clientId=skillhub&redirectUri=https://skillhub.company.com/auth/callback"
+ }
+}
+```
+
+#### 4.1.2 SSO 回调处理
+
+```http
+GET /api/auth/sso/callback
+```
+
+**Query 参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| `code` | String | 是 | 企业 SSO 返回的授权码 |
+| `state` | String | 是 | 防 CSRF 攻击的状态码 |
+
+**响应**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "user": {
+ "id": 123,
+ "username": "zhangbo",
+ "nickName": "张波",
+ "phone": "18217371537",
+ "tenantNo": "8000",
+ "tenantName": "盛虹石化",
+ "firstLogin": false,
+ "employments": [
+ {
+ "employeeId": 74,
+ "orgCodePath": "/60000000/66000078/66000022/",
+ "orgCodePathName": "/石化板块/原油供应链中心/盛虹新加坡/",
+ "positionCode": "63002474",
+ "positionName": "综合副经理"
+ }
+ ]
+ },
+ "accessToken": "AT-xxx",
+ "refreshToken": "RT-xxx",
+ "expiresIn": 1800
+ }
+}
+```
+
+#### 4.1.3 修改密码
+
+```http
+POST /api/auth/sso/change-password
+Content-Type: application/json
+Authorization: Bearer {accessToken}
+```
+
+**请求体**:
+
+```json
+{
+ "oldPassword": "OldPass123",
+ "newPassword": "NewPass456",
+ "logoutAfterChange": true
+}
+```
+
+**响应**:
+
+```json
+{
+ "success": true,
+ "message": "密码修改成功"
+}
+```
+
+#### 4.1.4 租户切换
+
+```http
+POST /api/auth/sso/switch-tenant
+Content-Type: application/json
+Authorization: Bearer {accessToken}
+```
+
+**请求体**:
+
+```json
+{
+ "targetTenantNo": "6000",
+ "refreshToken": "RT-xxx"
+}
+```
+
+**响应**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "accessToken": "AT-new-xxx",
+ "refreshToken": "RT-new-xxx",
+ "expiresIn": 1800,
+ "user": {
+ "tenantNo": "6000",
+ "tenantName": "江苏东方盛虹股份有限公司"
+ }
+ }
+}
+```
+
+#### 4.1.5 刷新 Token
+
+```http
+POST /api/auth/sso/refresh-token
+Content-Type: application/json
+```
+
+**请求体**:
+
+```json
+{
+ "refreshToken": "RT-xxx"
+}
+```
+
+**响应**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "accessToken": "AT-new-xxx",
+ "refreshToken": "RT-new-xxx",
+ "expiresIn": 1800
+ }
+}
+```
+
+#### 4.1.6 退出登录
+
+```http
+POST /api/auth/sso/logout
+Authorization: Bearer {accessToken}
+```
+
+**响应**:
+
+```json
+{
+ "success": true,
+ "message": "退出成功"
+}
+```
+
+### 4.2 前端接口(Hooks)
+
+#### 4.2.1 useEnterpriseLogin
+
+```typescript
+interface EnterpriseLoginOptions {
+ redirectUri?: string
+}
+
+interface EnterpriseLoginResult {
+ loginUrl: string
+ redirectToSso: () => void
+ isLoading: boolean
+ error: Error | null
+}
+
+export function useEnterpriseLogin(
+ options?: EnterpriseLoginOptions
+): EnterpriseLoginResult
+```
+
+#### 4.2.2 useSsoCallback
+
+```typescript
+interface SsoCallbackResult {
+ user: User | null
+ isLoading: boolean
+ error: Error | null
+ requirePasswordChange: boolean
+}
+
+export function useSsoCallback(
+ code: string | null,
+ state: string | null
+): SsoCallbackResult
+```
+
+#### 4.2.3 useTenantSwitch
+
+```typescript
+interface TenantSwitchOptions {
+ onSuccess?: (user: User) => void
+ onError?: (error: Error) => void
+}
+
+export function useTenantSwitch(
+ options?: TenantSwitchOptions
+): {
+ switchTenant: (tenantNo: string) => Promise
+ isLoading: boolean
+ error: Error | null
+}
+```
+
+---
+
+## 5. 数据模型设计
+
+### 5.1 企业 SSO 用户信息映射
+
+#### 企业 SSO 用户模型
+
+```typescript
+interface EnterpriseSsoUser {
+ id: number
+ employeeId: number
+ username: string
+ nickName: string
+ phone: string
+ email?: string
+ tenantNo: string
+ tenantName: string
+ roleCode: string
+ roleName: string
+ positionCode?: string
+ positionName?: string
+ firstLogin: boolean
+ tenantList: TenantInfo[]
+ employments: Employment[]
+}
+
+interface TenantInfo {
+ employeeId: number
+ empName: string
+ tenantNo: string
+ tenantName: string
+ position?: string
+}
+
+interface Employment {
+ employeeId: number
+ orgCodePath: string
+ orgCodePathName: string
+ positionCode: string
+ positionName: string
+}
+```
+
+#### SkillHub 平台用户模型
+
+```typescript
+interface SkillHubUser {
+ id: number
+ username: string
+ displayName: string
+ email?: string
+ phone?: string
+ status: UserStatus
+ roles: Role[]
+ createdAt: Date
+ updatedAt: Date
+}
+
+interface EnterpriseIdentityBinding {
+ id: number
+ userId: number // SkillHub 用户 ID
+ enterpriseUserId: number // 企业 SSO 用户 ID
+ employeeId: number // 员工 ID
+ tenantNo: string // 当前租户
+ provider: string // 'enterprise-sso'
+ createdAt: Date
+ updatedAt: Date
+}
+```
+
+### 5.2 Token 存储模型
+
+#### Redis 存储结构
+
+```
+# Access Token 存储
+Key: sso:access_token:{userId}:{tenantNo}
+Value: {
+ "token": "AT-xxx",
+ "expiresAt": 1680000000,
+ "scope": "read write"
+}
+TTL: 30 分钟
+
+# Refresh Token 存储
+Key: sso:refresh_token:{userId}
+Value: {
+ "token": "RT-xxx",
+ "expiresAt": 1680000000
+}
+TTL: 2 小时
+
+# 用户会话存储
+Key: session:{sessionId}
+Value: {
+ "userId": 123,
+ "tenantNo": "8000",
+ "accessToken": "AT-xxx",
+ "refreshToken": "RT-xxx",
+ "userInfo": {...}
+}
+TTL: 2 小时
+```
+
+---
+
+## 6. 安全设计
+
+### 6.1 Token 安全
+
+#### 6.1.1 Token 传输
+
+- **HTTPS Only**: 所有 Token 传输必须使用 HTTPS
+- **HttpOnly Cookie**: Refresh Token 存储在 HttpOnly Cookie 中,防止 XSS 攻击
+- **Header 传输**: Access Token 通过 Authorization Header 传输
+
+#### 6.1.2 Token 存储
+
+- **前端存储**: Access Token 存储在内存中(不使用 LocalStorage)
+- **后端存储**: Token 存储在 Redis,设置合理的过期时间
+- **加密存储**: 敏感 Token 使用 AES 加密后存储
+
+#### 6.1.3 Token 刷新
+
+- **自动刷新**: Access Token 过期前 5 分钟自动刷新
+- **静默刷新**: 后台刷新,不影响用户操作
+- **Refresh Token 轮转**: 每次刷新后更新 Refresh Token
+
+### 6.2 CSRF 防护
+
+- **State 参数**: OAuth 流程中使用 state 参数防止 CSRF
+- **CSRF Token**: 表单提交时验证 CSRF Token
+- **SameSite Cookie**: Cookie 设置 SameSite=Lax
+
+### 6.3 XSS 防护
+
+- **输入验证**: 所有用户输入进行验证和转义
+- **输出编码**: HTML 输出时进行编码
+- **CSP 策略**: 配置 Content-Security-Policy
+
+### 6.4 日志审计
+
+记录以下安全事件:
+
+- 登录成功/失败
+- Token 刷新
+- 租户切换
+- 密码修改
+- 退出登录
+- 异常访问
+
+---
+
+## 7. 实施步骤
+
+### 7.1 阶段一:基础框架搭建(1-2 天)
+
+**后端任务**:
+
+1. 创建 `EnterpriseDirectAuthProvider` 实现类
+2. 创建 `EnterpriseSsoClient` 用于调用企业 SSO API
+3. 配置 Spring Security Filter Chain
+4. 实现 `EnterpriseIdentityBinding` 数据模型和 Repository
+
+**前端任务**:
+
+1. 创建 SSO 登录入口组件
+2. 实现 `useEnterpriseLogin` Hook
+3. 创建 SSO 回调处理页面
+
+### 7.2 阶段二:核心流程实现(3-4 天)
+
+**后端任务**:
+
+1. 实现获取 SSO 登录地址接口
+2. 实现 SSO 回调处理逻辑
+3. 实现用户信息同步逻辑
+4. 实现 Token 刷新机制
+5. 集成 Redis 会话存储
+
+**前端任务**:
+
+1. 实现 `useSsoCallback` Hook
+2. 实现首次登录修改密码流程
+3. 实现 Token 自动刷新拦截器
+4. 更新全局认证状态管理
+
+### 7.3 阶段三:租户管理功能(2-3 天)
+
+**后端任务**:
+
+1. 实现租户切换接口
+2. 实现租户信息查询接口
+3. 实现多租户数据隔离
+
+**前端任务**:
+
+1. 实现租户切换组件
+2. 实现 `useTenantSwitch` Hook
+3. 实现租户信息展示
+
+### 7.4 阶段四:安全加固和测试(2-3 天)
+
+**安全加固**:
+
+1. 实现 Token 加密存储
+2. 配置 CSRF 防护
+3. 配置 CSP 策略
+4. 实现日志审计
+
+**测试**:
+
+1. 单元测试(覆盖率 80%+)
+2. 集成测试
+3. E2E 测试(关键流程)
+4. 安全测试
+
+### 7.5 阶段五:文档和部署(1 天)
+
+1. 编写接口文档
+2. 编写运维文档
+3. 配置生产环境
+4. 灰度发布
+
+---
+
+## 8. 配置管理
+
+### 8.1 后端配置(application.yml)
+
+```yaml
+skillhub:
+ auth:
+ enterprise-sso:
+ enabled: true
+ provider-code: "enterprise-sso"
+ display-name: "企业账号登录"
+ sso-base-url: "http://sso.company.com"
+ client-id: "skillhub"
+ client-secret: "${SSO_CLIENT_SECRET}"
+ callback-url: "https://skillhub.company.com/api/auth/sso/callback"
+ token-endpoint: "/auth/sso/login"
+ userinfo-endpoint: "/userinfo"
+ logout-endpoint: "/logout"
+ refresh-token-endpoint: "/auth/sso/refresh-token"
+ access-token-expire: 1800 # 30分钟
+ refresh-token-expire: 7200 # 2小时
+
+ redis:
+ host: ${REDIS_HOST:localhost}
+ port: ${REDIS_PORT:6379}
+ password: ${REDIS_PASSWORD:}
+ database: 0
+ timeout: 3000
+ lettuce:
+ pool:
+ max-active: 8
+ max-idle: 8
+ min-idle: 2
+```
+
+### 8.2 前端配置(.env)
+
+```bash
+# 企业 SSO 配置
+VITE_ENTERPRISE_SSO_ENABLED=true
+VITE_ENTERPRISE_SSO_DISPLAY_NAME=企业账号登录
+
+# API 配置
+VITE_API_BASE_URL=http://localhost:8080/api
+VITE_SSO_CALLBACK_PATH=/auth/sso/callback
+
+# Token 配置
+VITE_ACCESS_TOKEN_REFRESH_BEFORE_EXPIRE=300 # 5分钟
+```
+
+---
+
+## 9. 监控和告警
+
+### 9.1 监控指标
+
+- **认证成功率**: 登录成功次数 / 总登录次数
+- **Token 刷新成功率**: Token 刷新成功次数 / 总刷新次数
+- **SSO API 响应时间**: 调用企业 SSO API 的平均响应时间
+- **登录耗时**: 从点击登录到进入首页的平均耗时
+- **异常登录次数**: 失败次数、异常 IP 等
+
+### 9.2 告警规则
+
+- 认证成功率 < 95%,触发告警
+- SSO API 响应时间 > 3s,触发告警
+- Token 刷新失败率 > 10%,触发告警
+- 单用户 1 小时内登录失败 > 5 次,触发风控
+
+---
+
+## 10. 风险评估和应对
+
+### 10.1 潜在风险
+
+| 风险 | 影响 | 概率 | 应对措施 |
+|------|------|------|----------|
+| 企业 SSO 服务不可用 | 高 | 中 | 提供降级方案,允许管理员本地登录 |
+| Token 泄露 | 高 | 低 | Token 加密存储,设置短过期时间 |
+| 租户数据泄露 | 高 | 低 | 实现严格的数据隔离和权限控制 |
+| 性能问题 | 中 | 中 | Redis 缓存,减少 SSO API 调用 |
+| 兼容性问题 | 中 | 低 | 保留原有登录方式作为备选 |
+
+### 10.2 回滚方案
+
+- **配置开关**: 通过配置快速关闭企业 SSO
+- **数据库备份**: 实施前备份身份绑定数据
+- **灰度发布**: 小范围验证后再全量发布
+- **监控告警**: 实时监控异常并快速响应
+
+---
+
+## 11. FAQ
+
+### Q1: 企业 SSO 和原有登录方式是否可以共存?
+
+是的。设计支持多种登录方式共存:
+- 企业 SSO 登录(主要方式)
+- 本地账号登录(管理员备用)
+- OAuth2 第三方登录(保留)
+
+### Q2: 用户首次登录必须修改密码吗?
+
+根据企业安全策略决定。设计方案支持:
+- 强制修改密码(默认)
+- 可选修改密码
+- 不修改密码(不推荐)
+
+### Q3: 租户切换后原有的数据是否可见?
+
+不可见。租户切换后,用户只能访问当前租户的数据。如需访问其他租户数据,需要再次切换。
+
+### Q4: Token 过期后如何处理?
+
+自动刷新机制:
+- Access Token 过期前 5 分钟自动刷新
+- 过期后前端拦截器自动调用刷新接口
+- Refresh Token 过期则需要重新登录
+
+### Q5: 如何防止 CSRF 攻击?
+
+采用多重防护:
+- OAuth 流程中使用 state 参数
+- Cookie 设置 SameSite 属性
+- 关键操作验证 CSRF Token
+
+---
+
+## 12. 附录
+
+### 12.1 参考资料
+
+- [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
+- [Spring Security OAuth2](https://spring.io/projects/spring-security-oauth)
+- [企业 SSO 接口文档](./sh-sso-借口设计文档.md)
+
+### 12.2 相关流程图
+
+- [企业 SSO 首次登录流程](./企业SSO首次登录流程.md)
+- [企业 SSO 常规登录流程](./企业SSO常规登录流程.md)
+- [企业 SSO 租户切换流程](./企业SSO租户切换流程.md)
+- [企业 SSO 系统架构图](./企业SSO系统架构图.md)
+
+### 12.3 更新日志
+
+| 版本 | 日期 | 作者 | 变更说明 |
+|------|------|------|----------|
+| v1.0 | 2026-04-03 | Claude | 初始版本 |
+
+---
+
+**文档结束**
diff --git a/docs/sh-sso/企业SSO租户切换流程.md b/docs/sh-sso/企业SSO租户切换流程.md
new file mode 100644
index 00000000..0c7128e2
--- /dev/null
+++ b/docs/sh-sso/企业SSO租户切换流程.md
@@ -0,0 +1,590 @@
+# 企业 SSO 租户切换流程
+
+## 流程图
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant User as 用户浏览器
+ participant Frontend as SkillHub 前端
+ participant Backend as SkillHub 后端
+ participant SSO as 企业 SSO 系统
+ participant Redis as Redis 缓存
+ participant DB as 数据库
+
+ Note over User: 用户已登录,当前租户: 盛虹石化(8000)
+
+ User->>Frontend: 点击租户切换下拉菜单
+ Frontend->>Frontend: 从用户信息中读取 tenantList
+ Frontend->>User: 展示可切换的租户列表
+
+ Note over Frontend: tenantList 示例:
1. 盛虹石化 (8000) - 当前
2. 江苏东方盛虹 (6000)
+
+ User->>Frontend: 选择目标租户 "江苏东方盛虹 (6000)"
+ Frontend->>Frontend: 弹出确认对话框
"切换租户将刷新页面,是否继续?"
+
+ User->>Frontend: 点击"确定"
+
+ Frontend->>Frontend: 从 Cookie/内存中获取 Refresh Token
+ Frontend->>Backend: POST /api/auth/sso/switch-tenant
{
targetTenantNo: "6000",
refreshToken: "RT-xxx"
}
Header: Authorization: Bearer {currentToken}
+
+ Backend->>Redis: 验证当前 Access Token
+ alt Token 无效或过期
+ Backend-->>Frontend: 返回 401 Unauthorized
+ Frontend->>User: 提示"登录已过期,请重新登录"
+ Frontend->>Frontend: 跳转到登录页
+ end
+
+ Backend->>Backend: 从 Token 中解析当前用户 ID
+ Backend->>DB: 查询用户的租户列表
SELECT * FROM enterprise_identity_binding
WHERE user_id = ?
+
+ Backend->>Backend: 验证目标租户是否在允许列表中
+ alt 目标租户不在允许列表
+ Backend-->>Frontend: 返回 403 Forbidden
{message: "无权访问该租户"}
+ Frontend->>User: 显示错误提示
+ end
+
+ Backend->>SSO: POST /auth/sso/refresh-token
{
targetTenantNo: "6000",
refreshToken: "RT-xxx"
}
+
+ SSO->>SSO: 验证 Refresh Token
+ alt Refresh Token 无效或过期
+ SSO-->>Backend: 返回 401/415 (缺少或无效 Token)
+ Backend-->>Frontend: 返回 401
+ Frontend->>User: 提示"会话已过期,请重新登录"
+ Frontend->>Frontend: 清除本地认证状态
+ Frontend->>Frontend: 跳转到登录页
+ end
+
+ SSO->>SSO: 验证用户对目标租户的访问权限
+ alt 用户无权访问目标租户
+ SSO-->>Backend: 返回 403
+ Backend-->>Frontend: 返回 403
+ Frontend->>User: 提示"无权访问该租户"
+ end
+
+ SSO->>SSO: 生成新的 Token(包含目标租户信息)
+ SSO-->>Backend: 返回新的 Token 和用户信息
{
accessToken: "AT-new-xxx",
refreshToken: "RT-new-xxx",
expiresIn: 1800,
user: {
tenantNo: "6000",
tenantName: "江苏东方盛虹",
...
}
}
+
+ Backend->>Backend: 解析新 Token 中的用户信息
+ Backend->>DB: 更新身份绑定记录的当前租户
UPDATE enterprise_identity_binding
SET tenant_no = "6000", updated_at = NOW()
WHERE user_id = ?
+
+ Backend->>Redis: 清除旧租户的 Token 缓存
DEL sso:access_token:{userId}:8000
+ Backend->>Redis: 缓存新租户的 Access Token
SET sso:access_token:{userId}:6000 {token}
EXPIRE 1800
+
+ Backend->>Redis: 更新 Refresh Token 缓存
SET sso:refresh_token:{userId} {newRefreshToken}
EXPIRE 7200
+
+ Backend->>Redis: 更新用户会话
HSET session:{sessionId}
tenantNo "6000"
accessToken "AT-new-xxx"
refreshToken "RT-new-xxx"
+
+ Backend->>SSO: GET /userinfo
Header: Authorization: Bearer {newAccessToken}
+ SSO-->>Backend: 返回目标租户下的完整用户信息
(包含该租户的部门任职信息)
+
+ Backend->>DB: 更新用户在该租户的信息
UPDATE users SET
position_code = ?,
position_name = ?,
updated_at = NOW()
+
+ Backend->>Backend: 生成新的 SkillHub JWT Token
+ Backend-->>Frontend: 返回切换成功
{
success: true,
data: {
accessToken: "AT-new-xxx",
refreshToken: "RT-new-xxx",
user: {...},
message: "已切换到江苏东方盛虹"
}
}
+
+ Frontend->>Frontend: 更新内存中的 Token
+ Frontend->>Frontend: 更新全局用户状态
(包括租户信息、部门任职等)
+ Frontend->>Frontend: 清除所有 API 查询缓存
(React Query: queryClient.clear())
+
+ Frontend->>User: 显示成功提示
"已切换到江苏东方盛虹"
+ Frontend->>Frontend: 刷新当前页面
window.location.reload()
+
+ User->>Frontend: 页面重新加载
+ Frontend->>Backend: 重新请求数据
(使用新租户的 Token)
+
+ Backend->>DB: 查询该租户的数据
WHERE tenant_no = "6000"
+ Backend-->>Frontend: 返回新租户的数据
+
+ Frontend->>User: 展示新租户的页面内容
+
+ Note over User,Frontend: 租户切换完成
当前租户: 江苏东方盛虹(6000)
+```
+
+## 租户切换机制详解
+
+### 1. 租户列表来源
+
+用户的租户列表在登录时从 SSO 获取:
+
+```json
+{
+ "tenantList": [
+ {
+ "employeeId": 74,
+ "empName": "张波",
+ "tenantNo": "8000",
+ "tenantName": "盛虹石化",
+ "position": "综合副经理"
+ },
+ {
+ "employeeId": 66,
+ "empName": "用户-QGJNDWWD",
+ "tenantNo": "6000",
+ "tenantName": "江苏东方盛虹股份有限公司",
+ "position": "部门经理"
+ }
+ ]
+}
+```
+
+**前端展示逻辑**:
+
+```typescript
+// 租户下拉菜单组件
+function TenantSwitcher() {
+ const { user } = useAuth()
+ const { switchTenant, isLoading } = useTenantSwitch()
+
+ const currentTenant = user?.tenantNo
+ const tenantList = user?.tenantList || []
+
+ return (
+
+ )
+}
+```
+
+### 2. Token 刷新策略
+
+切换租户时,必须刷新 Token 的原因:
+
+1. **租户隔离**: Access Token 中包含租户信息,用于后端数据隔离
+2. **权限变更**: 不同租户下用户可能有不同的权限
+3. **审计追踪**: 新 Token 记录租户切换操作
+
+**Token 中的租户信息**:
+
+```json
+{
+ "userId": 123,
+ "username": "zhangbo",
+ "tenantNo": "6000", // 当前租户
+ "employeeId": 66, // 该租户下的员工 ID
+ "iat": 1680000000,
+ "exp": 1680001800
+}
+```
+
+### 3. 数据隔离机制
+
+#### 后端数据查询
+
+所有数据查询都带租户过滤:
+
+```java
+@RestController
+public class DataController {
+
+ @GetMapping("/api/projects")
+ public List getProjects(@AuthenticationPrincipal PlatformPrincipal principal) {
+ String tenantNo = principal.getTenantNo();
+ // 只查询当前租户的数据
+ return projectRepository.findByTenantNo(tenantNo);
+ }
+}
+```
+
+#### 数据库设计
+
+所有业务表都包含租户字段:
+
+```sql
+CREATE TABLE projects (
+ id BIGINT PRIMARY KEY,
+ tenant_no VARCHAR(32) NOT NULL, -- 租户号
+ name VARCHAR(255),
+ -- 其他字段...
+ INDEX idx_tenant (tenant_no)
+);
+```
+
+### 4. 缓存清除策略
+
+切换租户后需要清除的缓存:
+
+```typescript
+// 前端缓存清除
+async function switchTenant(tenantNo: string) {
+ // 1. 调用切换接口
+ const result = await api.switchTenant(tenantNo)
+
+ // 2. 清除 React Query 缓存
+ queryClient.clear()
+
+ // 3. 清除本地存储(如果有)
+ localStorage.removeItem('cached-data')
+
+ // 4. 重置全局状态
+ resetGlobalState()
+
+ // 5. 刷新页面
+ window.location.reload()
+}
+```
+
+```java
+// 后端缓存清除
+public void switchTenant(String userId, String newTenantNo) {
+ // 清除旧租户的数据缓存
+ redisTemplate.delete("user:" + userId + ":data:*");
+
+ // 清除旧租户的权限缓存
+ redisTemplate.delete("user:" + userId + ":permissions");
+
+ // 更新会话中的租户信息
+ updateSessionTenant(userId, newTenantNo);
+}
+```
+
+## 租户切换的前端实现
+
+### useTenantSwitch Hook
+
+```typescript
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { authApi } from '@/api/client'
+import type { User } from '@/api/types'
+
+interface TenantSwitchOptions {
+ onSuccess?: (user: User) => void
+ onError?: (error: Error) => void
+}
+
+export function useTenantSwitch(options?: TenantSwitchOptions) {
+ const queryClient = useQueryClient()
+
+ const mutation = useMutation({
+ mutationFn: async (targetTenantNo: string) => {
+ // 获取当前的 Refresh Token
+ const refreshToken = getRefreshToken()
+
+ if (!refreshToken) {
+ throw new Error('Refresh Token 不存在,请重新登录')
+ }
+
+ // 调用租户切换接口
+ return authApi.switchTenant({
+ targetTenantNo,
+ refreshToken
+ })
+ },
+
+ onSuccess: (data) => {
+ const { accessToken, refreshToken, user } = data
+
+ // 更新 Token
+ setAccessToken(accessToken)
+ setRefreshToken(refreshToken)
+
+ // 更新用户信息
+ queryClient.setQueryData(['auth', 'me'], user)
+
+ // 清除所有查询缓存
+ queryClient.clear()
+
+ // 回调
+ options?.onSuccess?.(user)
+
+ // 刷新页面以确保数据完全更新
+ setTimeout(() => {
+ window.location.reload()
+ }, 500)
+ },
+
+ onError: (error) => {
+ if (error.message.includes('401')) {
+ // Token 过期,跳转登录
+ redirectToLogin()
+ } else if (error.message.includes('403')) {
+ // 无权访问
+ showError('您无权访问该租户')
+ }
+
+ options?.onError?.(error)
+ }
+ })
+
+ return {
+ switchTenant: mutation.mutate,
+ isLoading: mutation.isPending,
+ error: mutation.error
+ }
+}
+```
+
+### 租户选择器组件
+
+```typescript
+import { useState } from 'react'
+import { Select, Modal, message } from 'antd'
+import { useAuth } from '@/features/auth/use-auth'
+import { useTenantSwitch } from '@/features/auth/use-tenant-switch'
+
+export function TenantSwitcher() {
+ const { user } = useAuth()
+ const { switchTenant, isLoading } = useTenantSwitch({
+ onSuccess: (newUser) => {
+ message.success(`已切换到 ${newUser.tenantName}`)
+ },
+ onError: (error) => {
+ message.error(error.message)
+ }
+ })
+
+ const [confirmVisible, setConfirmVisible] = useState(false)
+ const [targetTenant, setTargetTenant] = useState()
+
+ const handleTenantChange = (tenantNo: string) => {
+ if (tenantNo === user?.tenantNo) {
+ return // 当前租户,不切换
+ }
+
+ setTargetTenant(tenantNo)
+ setConfirmVisible(true)
+ }
+
+ const handleConfirm = () => {
+ if (targetTenant) {
+ switchTenant(targetTenant)
+ setConfirmVisible(false)
+ }
+ }
+
+ const currentTenant = user?.tenantNo
+ const tenantList = user?.tenantList || []
+
+ if (tenantList.length <= 1) {
+ // 只有一个租户,不显示切换器
+ return null
+ }
+
+ return (
+ <>
+
+
+ setConfirmVisible(false)}
+ okText="确定"
+ cancelText="取消"
+ >
+ 切换租户将刷新页面,未保存的数据可能会丢失。
+ 是否继续切换?
+
+ >
+ )
+}
+```
+
+## 后端实现
+
+### SsoController - 租户切换接口
+
+```java
+@RestController
+@RequestMapping("/api/auth/sso")
+public class SsoController {
+
+ @Autowired
+ private EnterpriseSsoClient ssoClient;
+
+ @Autowired
+ private IdentityBindingService identityBindingService;
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ @PostMapping("/switch-tenant")
+ public ResponseEntity switchTenant(
+ @RequestBody SwitchTenantRequest request,
+ @AuthenticationPrincipal PlatformPrincipal principal) {
+
+ String userId = principal.getUserId();
+ String targetTenantNo = request.getTargetTenantNo();
+ String refreshToken = request.getRefreshToken();
+
+ // 1. 验证用户是否有权访问目标租户
+ List allowedTenants = identityBindingService.getAllowedTenants(userId);
+ if (!allowedTenants.contains(targetTenantNo)) {
+ throw new AccessDeniedException("无权访问该租户");
+ }
+
+ // 2. 调用 SSO 刷新 Token
+ SsoTokenResponse tokenResponse = ssoClient.refreshToken(
+ targetTenantNo,
+ refreshToken
+ );
+
+ // 3. 更新身份绑定记录
+ identityBindingService.updateCurrentTenant(userId, targetTenantNo);
+
+ // 4. 清除旧租户缓存
+ clearTenantCache(userId);
+
+ // 5. 缓存新 Token
+ cacheTokens(userId, targetTenantNo, tokenResponse);
+
+ // 6. 获取目标租户的用户信息
+ User user = ssoClient.getUserInfo(tokenResponse.getAccessToken());
+
+ // 7. 更新用户信息
+ identityBindingService.updateUserInfo(userId, user);
+
+ // 8. 返回结果
+ return ResponseEntity.ok(SwitchTenantResponse.builder()
+ .accessToken(tokenResponse.getAccessToken())
+ .refreshToken(tokenResponse.getRefreshToken())
+ .expiresIn(tokenResponse.getExpiresIn())
+ .user(user)
+ .build());
+ }
+
+ private void clearTenantCache(String userId) {
+ // 清除数据缓存
+ redisTemplate.delete("user:" + userId + ":*");
+
+ // 清除权限缓存
+ redisTemplate.delete("permissions:" + userId);
+ }
+
+ private void cacheTokens(String userId, String tenantNo, SsoTokenResponse tokens) {
+ // 缓存 Access Token
+ String accessTokenKey = String.format("sso:access_token:%s:%s", userId, tenantNo);
+ redisTemplate.opsForValue().set(
+ accessTokenKey,
+ tokens.getAccessToken(),
+ tokens.getExpiresIn(),
+ TimeUnit.SECONDS
+ );
+
+ // 缓存 Refresh Token
+ String refreshTokenKey = String.format("sso:refresh_token:%s", userId);
+ redisTemplate.opsForValue().set(
+ refreshTokenKey,
+ tokens.getRefreshToken(),
+ tokens.getRefreshExpiresIn(),
+ TimeUnit.SECONDS
+ );
+ }
+}
+```
+
+## 安全考虑
+
+### 1. 权限验证
+
+严格验证用户对目标租户的访问权限:
+
+```java
+public boolean hasAccessToTenant(String userId, String tenantNo) {
+ // 查询用户的租户列表
+ List tenants = identityBindingRepository
+ .findTenantsByUserId(userId);
+
+ // 检查目标租户是否在列表中
+ return tenants.stream()
+ .anyMatch(t -> t.getTenantNo().equals(tenantNo));
+}
+```
+
+### 2. 审计日志
+
+记录所有租户切换操作:
+
+```java
+@Aspect
+public class TenantSwitchAudit {
+
+ @AfterReturning("execution(* switchTenant(..))")
+ public void auditTenantSwitch(JoinPoint joinPoint) {
+ SwitchTenantRequest request = (SwitchTenantRequest) joinPoint.getArgs()[0];
+ PlatformPrincipal principal = (PlatformPrincipal) joinPoint.getArgs()[1];
+
+ auditLog.info("用户 {} 从租户 {} 切换到租户 {}",
+ principal.getUserId(),
+ principal.getTenantNo(),
+ request.getTargetTenantNo()
+ );
+ }
+}
+```
+
+### 3. 频率限制
+
+防止恶意频繁切换:
+
+```java
+public void switchTenant(...) {
+ String rateLimitKey = "tenant:switch:limit:" + userId;
+ Long count = redisTemplate.opsForValue().increment(rateLimitKey);
+
+ if (count == 1) {
+ // 首次设置过期时间
+ redisTemplate.expire(rateLimitKey, 1, TimeUnit.MINUTES);
+ }
+
+ if (count > 10) {
+ throw new RateLimitException("切换过于频繁,请稍后再试");
+ }
+
+ // ... 正常切换逻辑
+}
+```
+
+## 测试用例
+
+### 正常流程测试
+
+- [x] 切换到有权限的租户成功
+- [x] 数据正确隔离
+- [x] Token 正确刷新
+- [x] 用户信息正确更新
+- [x] 缓存正确清除
+
+### 异常流程测试
+
+- [x] 切换到无权限租户被拒绝
+- [x] Refresh Token 过期跳转登录
+- [x] 频繁切换触发限流
+- [x] SSO 系统不可用
+- [x] 并发切换处理
+
+### 性能测试
+
+- [x] 100 用户同时切换租户
+- [x] 切换后首页加载速度
+- [x] 缓存清除性能影响
+
+---
+
+**相关文档**:
+- [企业 SSO 登录接入方案设计](./企业SSO登录接入方案设计.md)
+- [企业 SSO 首次登录流程](./企业SSO首次登录流程.md)
+- [企业 SSO 常规登录流程](./企业SSO常规登录流程.md)
diff --git a/docs/sh-sso/企业SSO系统架构图.md b/docs/sh-sso/企业SSO系统架构图.md
new file mode 100644
index 00000000..0a570a09
--- /dev/null
+++ b/docs/sh-sso/企业SSO系统架构图.md
@@ -0,0 +1,592 @@
+# 企业 SSO 系统架构图
+
+## 总体架构
+
+```mermaid
+graph TB
+ subgraph "用户层"
+ U1[用户浏览器]
+ U2[移动端 App]
+ end
+
+ subgraph "前端层"
+ FE1[React 应用]
+ FE2[登录页面]
+ FE3[SSO 回调页面]
+ FE4[租户切换组件]
+ end
+
+ subgraph "网关层"
+ GW[API Gateway / Nginx]
+ end
+
+ subgraph "后端服务层"
+ subgraph "认证服务"
+ AS1[SsoController]
+ AS2[EnterpriseDirectAuthProvider]
+ AS3[SecurityFilterChain]
+ end
+
+ subgraph "业务服务"
+ BS1[User Service]
+ BS2[Project Service]
+ BS3[其他业务服务]
+ end
+
+ subgraph "核心服务"
+ CS1[IdentityBindingService]
+ CS2[TokenService]
+ CS3[TenantService]
+ end
+ end
+
+ subgraph "数据层"
+ DB[(MySQL Database)]
+ Redis[(Redis Cache)]
+ end
+
+ subgraph "外部系统"
+ SSO[企业 SSO 认证系统]
+ end
+
+ U1 --> FE1
+ U2 --> FE1
+ FE1 --> FE2
+ FE1 --> FE3
+ FE1 --> FE4
+
+ FE1 --> GW
+ FE2 --> GW
+ FE3 --> GW
+ FE4 --> GW
+
+ GW --> AS1
+ GW --> BS1
+ GW --> BS2
+ GW --> BS3
+
+ AS1 --> AS2
+ AS1 --> AS3
+ AS2 --> CS1
+ AS2 --> CS2
+ AS1 --> CS3
+
+ BS1 --> CS3
+ BS2 --> CS3
+
+ CS1 --> DB
+ CS2 --> Redis
+ CS3 --> DB
+ CS3 --> Redis
+
+ AS2 --> SSO
+ AS1 --> SSO
+```
+
+## 认证流程架构
+
+```mermaid
+flowchart LR
+ subgraph "请求入口"
+ A[HTTP Request]
+ end
+
+ subgraph "Spring Security Filter Chain"
+ B[ApiTokenAuthFilter]
+ C[SessionAuthFilter]
+ D[SsoAuthFilter]
+ E[AnonymousAuthFilter]
+ end
+
+ subgraph "认证提供者"
+ F[ApiTokenAuthProvider]
+ G[SessionAuthProvider]
+ H[EnterpriseDirectAuthProvider]
+ end
+
+ subgraph "认证结果"
+ I[PlatformPrincipal]
+ J[Authentication Success]
+ K[Authentication Failure]
+ end
+
+ A --> B
+ B -->|有 API Token| F
+ B -->|无 API Token| C
+
+ C -->|有 Session| G
+ C -->|无 Session| D
+
+ D -->|SSO Token| H
+ D -->|无 Token| E
+
+ F --> I
+ G --> I
+ H --> I
+
+ I --> J
+ E --> K
+```
+
+## Token 生命周期管理
+
+```mermaid
+stateDiagram-v2
+ [*] --> 未登录
+
+ 未登录 --> SSO认证: 用户点击登录
+
+ SSO认证 --> Token签发: SSO 验证成功
+ Token签发 --> Token有效: 签发 Access + Refresh Token
+
+ Token有效 --> Token即将过期: 使用 < 5分钟
+ Token即将过期 --> 自动刷新: 后台刷新
+ 自动刷新 --> Token有效: 刷新成功
+
+ Token有效 --> Token过期: 超过有效期
+ Token过期 --> 尝试刷新: 前端拦截 401
+ 尝试刷新 --> Token有效: Refresh Token 有效
+ 尝试刷新 --> 未登录: Refresh Token 过期
+
+ Token有效 --> 租户切换: 用户切换租户
+ 租户切换 --> Token签发: 刷新为新租户 Token
+
+ Token有效 --> 未登录: 用户主动登出
+```
+
+## 数据库设计
+
+```mermaid
+erDiagram
+ users ||--o{ enterprise_identity_binding : has
+ users ||--o{ user_role_binding : has
+ roles ||--o{ user_role_binding : has
+ roles ||--o{ role_permission : has
+ permissions ||--o{ role_permission : has
+ users ||--o{ api_tokens : creates
+
+ users {
+ bigint id PK
+ string username UK
+ string display_name
+ string email
+ string phone
+ string status
+ timestamp created_at
+ timestamp updated_at
+ }
+
+ enterprise_identity_binding {
+ bigint id PK
+ bigint user_id FK
+ bigint enterprise_user_id
+ bigint employee_id
+ string tenant_no
+ string provider
+ timestamp created_at
+ timestamp updated_at
+ }
+
+ user_role_binding {
+ bigint id PK
+ bigint user_id FK
+ bigint role_id FK
+ string tenant_no
+ timestamp created_at
+ }
+
+ roles {
+ bigint id PK
+ string code UK
+ string name
+ string description
+ timestamp created_at
+ }
+
+ role_permission {
+ bigint id PK
+ bigint role_id FK
+ bigint permission_id FK
+ }
+
+ permissions {
+ bigint id PK
+ string code UK
+ string name
+ string resource
+ string action
+ timestamp created_at
+ }
+
+ api_tokens {
+ bigint id PK
+ bigint user_id FK
+ string token_hash
+ string scope
+ timestamp expires_at
+ timestamp created_at
+ }
+```
+
+## Redis 缓存架构
+
+```mermaid
+graph TB
+ subgraph "Token 缓存"
+ T1["Key: sso:access_token:{userId}:{tenantNo}
Value: Access Token
TTL: 30分钟"]
+ T2["Key: sso:refresh_token:{userId}
Value: Refresh Token
TTL: 2小时"]
+ end
+
+ subgraph "会话缓存"
+ S1["Key: session:{sessionId}
Value: Session Data
TTL: 2小时"]
+ end
+
+ subgraph "用户信息缓存"
+ U1["Key: user:info:{userId}
Value: User Profile
TTL: 1小时"]
+ U2["Key: user:tenants:{userId}
Value: Tenant List
TTL: 1小时"]
+ end
+
+ subgraph "权限缓存"
+ P1["Key: permissions:{userId}:{tenantNo}
Value: Permission List
TTL: 30分钟"]
+ end
+
+ subgraph "限流缓存"
+ R1["Key: rate:limit:login:{ip}
Value: Counter
TTL: 5分钟"]
+ R2["Key: rate:limit:switch:{userId}
Value: Counter
TTL: 1分钟"]
+ end
+
+ subgraph "防重放缓存"
+ A1["Key: auth:state:{state}
Value: Nonce
TTL: 5分钟"]
+ end
+```
+
+## 多租户数据隔离
+
+```mermaid
+flowchart TB
+ subgraph "请求处理"
+ A[API Request]
+ B[Extract Token]
+ C[Parse Tenant Info]
+ end
+
+ subgraph "租户上下文"
+ D[TenantContext]
+ E[ThreadLocal]
+ end
+
+ subgraph "数据访问层"
+ F[MyBatis Interceptor]
+ G[Auto Inject tenant_no]
+ H[SQL: WHERE tenant_no = ?]
+ end
+
+ subgraph "数据库"
+ I[(Multi-Tenant Data)]
+ end
+
+ A --> B
+ B --> C
+ C --> D
+ D --> E
+
+ E --> F
+ F --> G
+ G --> H
+ H --> I
+
+ style D fill:#f9f,stroke:#333
+ style E fill:#f9f,stroke:#333
+```
+
+## 安全防护层次
+
+```mermaid
+graph TB
+ subgraph "网络层"
+ N1[HTTPS/TLS 1.3]
+ N2[DDoS 防护]
+ N3[WAF 防火墙]
+ end
+
+ subgraph "应用层"
+ A1[CSRF Token]
+ A2[XSS 过滤]
+ A3[SQL 注入防护]
+ A4[参数验证]
+ end
+
+ subgraph "认证层"
+ AU1[Token 加密]
+ AU2[Token 签名验证]
+ AU3[State 参数验证]
+ AU4[Refresh Token 轮转]
+ end
+
+ subgraph "授权层"
+ AZ1[RBAC 权限控制]
+ AZ2[租户隔离]
+ AZ3[API 权限验证]
+ end
+
+ subgraph "审计层"
+ L1[访问日志]
+ L2[操作审计]
+ L3[异常告警]
+ end
+
+ N1 --> A1
+ N2 --> A2
+ N3 --> A3
+ A4 --> AU1
+ AU2 --> AZ1
+ AU3 --> AZ2
+ AU4 --> AZ3
+ AZ1 --> L1
+ AZ2 --> L2
+ AZ3 --> L3
+```
+
+## 部署架构
+
+```mermaid
+graph TB
+ subgraph "负载均衡层"
+ LB[Nginx / ALB]
+ end
+
+ subgraph "应用服务器集群"
+ APP1[SkillHub Server 1]
+ APP2[SkillHub Server 2]
+ APP3[SkillHub Server 3]
+ end
+
+ subgraph "数据库集群"
+ subgraph "主从复制"
+ DBM[(MySQL Master)]
+ DBS1[(MySQL Slave 1)]
+ DBS2[(MySQL Slave 2)]
+ end
+ end
+
+ subgraph "缓存集群"
+ subgraph "Redis Sentinel"
+ RM[(Redis Master)]
+ RS1[(Redis Slave 1)]
+ RS2[(Redis Slave 2)]
+ end
+ end
+
+ subgraph "外部依赖"
+ SSO[企业 SSO 系统]
+ end
+
+ subgraph "监控系统"
+ M1[Prometheus]
+ M2[Grafana]
+ M3[AlertManager]
+ end
+
+ LB --> APP1
+ LB --> APP2
+ LB --> APP3
+
+ APP1 --> DBM
+ APP2 --> DBM
+ APP3 --> DBM
+
+ DBM --> DBS1
+ DBM --> DBS2
+
+ APP1 --> RM
+ APP2 --> RM
+ APP3 --> RM
+
+ RM --> RS1
+ RM --> RS2
+
+ APP1 --> SSO
+ APP2 --> SSO
+ APP3 --> SSO
+
+ APP1 --> M1
+ APP2 --> M1
+ APP3 --> M1
+
+ M1 --> M2
+ M1 --> M3
+```
+
+## 关键组件说明
+
+### 1. EnterpriseDirectAuthProvider
+
+**职责**:
+- 实现 `DirectAuthProvider` 接口
+- 对接企业 SSO 认证 API
+- Token 获取与验证
+- 用户信息映射
+
+**核心方法**:
+
+```java
+public interface DirectAuthProvider {
+ String providerCode();
+ PlatformPrincipal authenticate(DirectAuthRequest request);
+}
+
+@Service
+public class EnterpriseDirectAuthProvider implements DirectAuthProvider {
+
+ @Override
+ public String providerCode() {
+ return "enterprise-sso";
+ }
+
+ @Override
+ public PlatformPrincipal authenticate(DirectAuthRequest request) {
+ // 1. 调用 SSO 登录 API
+ // 2. 获取用户信息
+ // 3. 绑定或创建平台账号
+ // 4. 返回 PlatformPrincipal
+ }
+}
+```
+
+### 2. IdentityBindingService
+
+**职责**:
+- 企业账号与平台账号的绑定关系管理
+- 用户信息同步
+- 租户信息管理
+
+**核心方法**:
+
+```java
+@Service
+public class IdentityBindingService {
+
+ // 绑定或创建账号
+ PlatformPrincipal bindOrCreate(OAuthClaims claims, UserStatus status);
+
+ // 查询允许访问的租户列表
+ List getAllowedTenants(String userId);
+
+ // 更新当前租户
+ void updateCurrentTenant(String userId, String tenantNo);
+
+ // 同步用户信息
+ void syncUserInfo(String userId, EnterpriseSsoUser ssoUser);
+}
+```
+
+### 3. TokenService
+
+**职责**:
+- Token 缓存管理
+- Token 刷新逻辑
+- Token 验证
+
+**核心方法**:
+
+```java
+@Service
+public class TokenService {
+
+ // 缓存 Token
+ void cacheToken(String userId, String tenantNo, TokenPair tokens);
+
+ // 获取 Token
+ Optional getAccessToken(String userId, String tenantNo);
+
+ // 刷新 Token
+ TokenPair refreshToken(String userId, String refreshToken);
+
+ // 验证 Token
+ boolean validateToken(String token);
+}
+```
+
+### 4. TenantService
+
+**职责**:
+- 租户上下文管理
+- 租户数据隔离
+- 租户权限验证
+
+**核心方法**:
+
+```java
+@Service
+public class TenantService {
+
+ // 设置当前租户上下文
+ void setCurrentTenant(String tenantNo);
+
+ // 获取当前租户
+ String getCurrentTenant();
+
+ // 验证租户访问权限
+ boolean hasAccessToTenant(String userId, String tenantNo);
+
+ // 清除租户上下文
+ void clearTenantContext();
+}
+```
+
+## 性能优化策略
+
+### 1. 缓存策略
+
+- **多级缓存**: 本地缓存 (Caffeine) + Redis
+- **缓存预热**: 启动时预加载热点数据
+- **缓存更新**: 使用 Redis Pub/Sub 同步多实例
+
+### 2. 数据库优化
+
+- **读写分离**: 查询走从库,写入走主库
+- **连接池**: 使用 HikariCP,合理配置连接数
+- **索引优化**: tenant_no、user_id 等常用字段建索引
+
+### 3. 异步处理
+
+- **用户信息同步**: 异步更新用户信息
+- **审计日志**: 异步写入日志
+- **通知发送**: 使用消息队列异步发送
+
+### 4. 限流熔断
+
+- **接口限流**: 使用 Guava RateLimiter 或 Sentinel
+- **熔断降级**: 企业 SSO 不可用时启用降级方案
+- **超时控制**: 设置合理的 HTTP 超时时间
+
+## 监控指标
+
+### 1. 业务指标
+
+- 登录成功率
+- 登录耗时
+- Token 刷新成功率
+- 租户切换成功率
+
+### 2. 技术指标
+
+- API 响应时间
+- 数据库连接数
+- Redis 命中率
+- JVM 内存使用
+
+### 3. 安全指标
+
+- 登录失败次数
+- 异常 IP 访问
+- Token 泄露检测
+- 权限越权尝试
+
+---
+
+**相关文档**:
+- [企业 SSO 登录接入方案设计](./企业SSO登录接入方案设计.md)
+- [企业 SSO 首次登录流程](./企业SSO首次登录流程.md)
+- [企业 SSO 常规登录流程](./企业SSO常规登录流程.md)
+- [企业 SSO 租户切换流程](./企业SSO租户切换流程.md)
diff --git a/docs/sh-sso/企业SSO首次登录流程.md b/docs/sh-sso/企业SSO首次登录流程.md
new file mode 100644
index 00000000..0820df13
--- /dev/null
+++ b/docs/sh-sso/企业SSO首次登录流程.md
@@ -0,0 +1,226 @@
+# 企业 SSO 首次登录流程
+
+## 流程图
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant User as 用户浏览器
+ participant Frontend as SkillHub 前端
+ participant Backend as SkillHub 后端
+ participant SSO as 企业 SSO 系统
+ participant Redis as Redis 缓存
+ participant DB as 数据库
+
+ User->>Frontend: 访问登录页
+ Frontend->>User: 展示登录页面(显示"企业账号登录"按钮)
+
+ User->>Frontend: 点击"企业账号登录"
+ Frontend->>Backend: GET /api/auth/sso/login-url?redirectUri={callback}
+
+ Backend->>Backend: 生成 state 参数(防 CSRF)
+ Backend->>Redis: 缓存 state (TTL: 5分钟)
+ Backend->>Backend: 构建 SSO 登录 URL
+ Backend-->>Frontend: 返回 SSO 登录地址
+
+ Frontend->>User: 重定向到企业 SSO 登录页
+ Note over User,SSO: 跳转到企业 SSO 认证系统
+
+ User->>SSO: 访问 SSO 登录页
+ SSO->>User: 展示登录表单
+
+ User->>SSO: 输入企业账号和密码
+ SSO->>SSO: 验证用户凭证
+
+ alt 认证失败
+ SSO->>User: 显示错误信息
+ User->>SSO: 重新输入
+ end
+
+ SSO->>SSO: 认证成功,生成授权码 (code)
+ SSO->>User: 重定向回 SkillHub callback (携带 code 和 state)
+
+ User->>Frontend: 访问 /auth/sso/callback?code=xxx&state=yyy
+ Frontend->>Backend: POST /api/auth/sso/callback
{code, state}
+
+ Backend->>Redis: 验证 state 参数
+ alt state 无效或过期
+ Backend-->>Frontend: 返回错误(CSRF 攻击)
+ Frontend->>User: 显示错误,重新登录
+ end
+
+ Backend->>SSO: POST /auth/sso/login
使用 code 换取 Token
+ SSO->>SSO: 验证授权码
+ SSO-->>Backend: 返回 Access Token + Refresh Token + 用户信息
+
+ Backend->>Backend: 解析用户信息
+ Backend->>SSO: GET /userinfo
获取完整用户信息
+ SSO-->>Backend: 返回用户详细信息(包含租户列表、部门任职等)
+
+ Backend->>Backend: 判断 firstLogin 标记
+
+ alt firstLogin = true (首次登录)
+ Backend->>Redis: 缓存 Token 和用户信息(临时)
+ Backend-->>Frontend: 返回 {requirePasswordChange: true, tempToken}
+
+ Frontend->>User: 显示"首次登录,请修改密码"提示
+ User->>Frontend: 输入新密码
+ Frontend->>Backend: POST /api/auth/sso/change-password
{tempToken, newPassword, logoutAfterChange: false}
+
+ Backend->>SSO: POST /password/save-password
修改密码
+ SSO->>SSO: 验证密码强度
+
+ alt 密码不符合要求
+ SSO-->>Backend: 返回错误(密码必须包含大小写字母和数字,长度8-20位)
+ Backend-->>Frontend: 返回错误
+ Frontend->>User: 显示密码要求,重新输入
+ end
+
+ SSO-->>Backend: 密码修改成功
+ Backend->>DB: 创建平台账号
+ Backend->>DB: 创建身份绑定记录
(EnterpriseIdentityBinding)
+ Backend->>DB: 分配默认角色
+ Backend->>Redis: 缓存正式 Token 和会话
+ Backend-->>Frontend: 返回登录成功 {user, token}
+ else firstLogin = false
+ Note over Backend: 这种情况不应该出现在首次登录流程
+ Backend-->>Frontend: 返回错误
+ end
+
+ Frontend->>Frontend: 保存 Token (内存)
+ Frontend->>Frontend: 更新全局认证状态
+ Frontend->>User: 跳转到首页
+
+ User->>Frontend: 访问首页
+ Frontend->>Backend: GET /api/user/profile
Header: Authorization: Bearer {token}
+ Backend->>Redis: 验证 Token
+ Backend->>DB: 查询用户信息
+ Backend-->>Frontend: 返回用户信息
+ Frontend->>User: 展示首页内容
+```
+
+## 关键步骤说明
+
+### 1. 前端跳转 SSO
+
+- 前端调用后端接口获取 SSO 登录 URL
+- 后端生成唯一的 `state` 参数用于防 CSRF 攻击
+- 前端执行 `window.location.href = ssoLoginUrl` 跳转
+
+### 2. SSO 认证
+
+- 用户在企业 SSO 系统输入账号密码
+- SSO 验证通过后生成授权码(code)
+- SSO 重定向回 SkillHub 回调地址
+
+### 3. 授权码换 Token
+
+- SkillHub 后端接收授权码
+- 验证 state 参数防止 CSRF
+- 使用授权码向 SSO 换取 Access Token 和 Refresh Token
+
+### 4. 首次登录处理
+
+**判断依据**: `user.firstLogin === true`
+
+**处理流程**:
+1. 后端返回 `requirePasswordChange: true`
+2. 前端显示修改密码表单
+3. 用户输入新密码
+4. 调用密码修改接口
+5. SSO 验证密码强度(必须包含大小写字母和数字,8-20位)
+6. 修改成功后创建平台账号并绑定
+7. 返回正式 Token,完成登录
+
+### 5. 账号绑定
+
+创建以下绑定关系:
+
+```sql
+INSERT INTO enterprise_identity_binding (
+ user_id,
+ enterprise_user_id,
+ employee_id,
+ tenant_no,
+ provider,
+ created_at
+) VALUES (
+ 123, -- SkillHub 平台用户 ID
+ 74, -- 企业 SSO 用户 ID
+ 74, -- 员工 ID
+ '8000', -- 当前租户号
+ 'enterprise-sso', -- 认证提供方
+ NOW()
+);
+```
+
+### 6. Token 缓存
+
+Redis 缓存结构:
+
+```
+# 用户会话
+Key: session:{userId}
+Value: {
+ "accessToken": "AT-xxx",
+ "refreshToken": "RT-xxx",
+ "tenantNo": "8000",
+ "expiresAt": 1680001800
+}
+TTL: 1800 (30分钟)
+```
+
+## 错误处理
+
+### 常见错误场景
+
+| 错误码 | 场景 | 用户提示 | 处理方式 |
+|--------|------|----------|----------|
+| 401 | 用户不存在或密码不正确 | "账号或密码错误,请重试" | 返回登录页 |
+| 403 | state 参数无效 (CSRF) | "登录链接已失效,请重新登录" | 重新获取登录 URL |
+| 406 | 密码不符合强度要求 | "密码必须包含大小写字母和数字,长度8-20位" | 提示重新输入 |
+| 415 | Token 缺失或无效 | "登录已过期,请重新登录" | 跳转登录页 |
+| 500 | SSO 系统异常 | "系统繁忙,请稍后重试" | 提供降级方案 |
+
+## 安全要点
+
+1. **HTTPS Only**: 所有请求必须使用 HTTPS
+2. **State 验证**: 严格验证 state 参数防止 CSRF
+3. **Token 安全**:
+ - Access Token 存储在内存(不用 LocalStorage)
+ - Refresh Token 存储在 HttpOnly Cookie
+4. **密码强度**: 强制8-20位,必须包含大小写字母和数字
+5. **Session 过期**: Access Token 30分钟过期,自动刷新
+6. **日志审计**: 记录所有登录行为
+
+## 性能优化
+
+1. **Redis 缓存**: 缓存 Token 和用户信息,减少数据库查询
+2. **并发处理**: 使用分布式锁防止重复创建账号
+3. **异步处理**: 用户信息同步采用异步方式
+4. **连接池**: 配置合理的 Redis 和数据库连接池
+
+## 测试用例
+
+### 正常流程测试
+
+- [x] 新用户首次登录成功
+- [x] 修改密码成功
+- [x] 账号绑定成功
+- [x] Token 缓存成功
+- [x] 跳转首页成功
+
+### 异常流程测试
+
+- [x] SSO 登录失败(账号密码错误)
+- [x] State 参数被篡改(CSRF 攻击)
+- [x] 密码强度不符合要求
+- [x] SSO 系统不可用(超时)
+- [x] 重复创建账号(并发)
+
+---
+
+**相关文档**:
+- [企业 SSO 登录接入方案设计](./企业SSO登录接入方案设计.md)
+- [企业 SSO 常规登录流程](./企业SSO常规登录流程.md)
+- [企业 SSO 租户切换流程](./企业SSO租户切换流程.md)