mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-04 02:34:22 +00:00
feat : 新增盛虹sso登陆设计文档
This commit is contained in:
parent
fa981f455c
commit
86c0266cc4
7 changed files with 3296 additions and 0 deletions
313
docs/sh-sso/README.md
Normal file
313
docs/sh-sso/README.md
Normal file
|
|
@ -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 索引
|
||||
|
||||
所有文档已完成!✅
|
||||
415
docs/sh-sso/sh-sso-借口设计文档.md
Normal file
415
docs/sh-sso/sh-sso-借口设计文档.md
Normal file
|
|
@ -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/...`。
|
||||
330
docs/sh-sso/企业SSO常规登录流程.md
Normal file
330
docs/sh-sso/企业SSO常规登录流程.md
Normal file
|
|
@ -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<br/>{code, state}
|
||||
|
||||
Backend->>Redis: 验证 state 参数
|
||||
alt state 无效
|
||||
Backend-->>Frontend: 返回 CSRF 错误
|
||||
Frontend->>User: 提示重新登录
|
||||
end
|
||||
|
||||
Backend->>SSO: POST /auth/sso/login<br/>使用 code 换取 Token
|
||||
SSO-->>Backend: 返回 Access Token + Refresh Token
|
||||
|
||||
Backend->>Backend: 解析 Token 中的用户 ID
|
||||
Backend->>DB: 查询身份绑定记录<br/>WHERE enterprise_user_id = ?
|
||||
|
||||
alt 绑定记录不存在
|
||||
Note over Backend: 用户从未登录过,应走首次登录流程
|
||||
Backend-->>Frontend: 返回错误(未绑定)
|
||||
Frontend->>User: 提示联系管理员
|
||||
end
|
||||
|
||||
Backend->>SSO: GET /userinfo<br/>Header: Authorization: Bearer {token}
|
||||
SSO-->>Backend: 返回最新用户信息
|
||||
|
||||
Backend->>DB: 更新用户信息<br/>UPDATE users SET<br/>nick_name=?, phone=?, updated_at=NOW()
|
||||
Backend->>DB: 更新身份绑定信息<br/>UPDATE enterprise_identity_binding<br/>SET tenant_no=?, updated_at=NOW()
|
||||
|
||||
Backend->>Redis: 缓存 Access Token<br/>Key: sso:access_token:{userId}<br/>TTL: 1800 (30分钟)
|
||||
Backend->>Redis: 缓存 Refresh Token<br/>Key: sso:refresh_token:{userId}<br/>TTL: 7200 (2小时)
|
||||
Backend->>Redis: 缓存用户会话<br/>Key: session:{sessionId}<br/>TTL: 7200
|
||||
|
||||
Backend->>Backend: 生成 SkillHub JWT Token
|
||||
Backend-->>Frontend: 返回登录成功<br/>{user, token, requirePasswordChange: false}
|
||||
|
||||
Frontend->>Frontend: 保存 Token 到内存
|
||||
Frontend->>Frontend: 更新全局认证状态
|
||||
|
||||
alt 有 returnTo 参数
|
||||
Frontend->>User: 跳转到指定页面
|
||||
else 无 returnTo 参数
|
||||
Frontend->>User: 跳转到首页
|
||||
end
|
||||
|
||||
User->>Frontend: 访问目标页面
|
||||
Frontend->>Backend: API 请求<br/>Header: Authorization: Bearer {token}
|
||||
|
||||
Backend->>Redis: 验证 Token 有效性
|
||||
alt Token 即将过期(剩余时间 < 5分钟)
|
||||
Backend->>Backend: 触发自动刷新机制
|
||||
Backend->>SSO: POST /auth/sso/refresh-token<br/>{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<br/>{refreshToken}
|
||||
SSO->>SSO: 验证 Refresh Token
|
||||
|
||||
alt Refresh Token 有效
|
||||
SSO-->>Backend: 返回新 Access Token
|
||||
Backend->>Redis: 更新缓存<br/>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)
|
||||
830
docs/sh-sso/企业SSO登录接入方案设计.md
Normal file
830
docs/sh-sso/企业SSO登录接入方案设计.md
Normal file
|
|
@ -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<void>
|
||||
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 | 初始版本 |
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
590
docs/sh-sso/企业SSO租户切换流程.md
Normal file
590
docs/sh-sso/企业SSO租户切换流程.md
Normal file
|
|
@ -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 示例:<br/>1. 盛虹石化 (8000) - 当前<br/>2. 江苏东方盛虹 (6000)
|
||||
|
||||
User->>Frontend: 选择目标租户 "江苏东方盛虹 (6000)"
|
||||
Frontend->>Frontend: 弹出确认对话框<br/>"切换租户将刷新页面,是否继续?"
|
||||
|
||||
User->>Frontend: 点击"确定"
|
||||
|
||||
Frontend->>Frontend: 从 Cookie/内存中获取 Refresh Token
|
||||
Frontend->>Backend: POST /api/auth/sso/switch-tenant<br/>{<br/> targetTenantNo: "6000",<br/> refreshToken: "RT-xxx"<br/>}<br/>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: 查询用户的租户列表<br/>SELECT * FROM enterprise_identity_binding<br/>WHERE user_id = ?
|
||||
|
||||
Backend->>Backend: 验证目标租户是否在允许列表中
|
||||
alt 目标租户不在允许列表
|
||||
Backend-->>Frontend: 返回 403 Forbidden<br/>{message: "无权访问该租户"}
|
||||
Frontend->>User: 显示错误提示
|
||||
end
|
||||
|
||||
Backend->>SSO: POST /auth/sso/refresh-token<br/>{<br/> targetTenantNo: "6000",<br/> refreshToken: "RT-xxx"<br/>}
|
||||
|
||||
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 和用户信息<br/>{<br/> accessToken: "AT-new-xxx",<br/> refreshToken: "RT-new-xxx",<br/> expiresIn: 1800,<br/> user: {<br/> tenantNo: "6000",<br/> tenantName: "江苏东方盛虹",<br/> ...<br/> }<br/>}
|
||||
|
||||
Backend->>Backend: 解析新 Token 中的用户信息
|
||||
Backend->>DB: 更新身份绑定记录的当前租户<br/>UPDATE enterprise_identity_binding<br/>SET tenant_no = "6000", updated_at = NOW()<br/>WHERE user_id = ?
|
||||
|
||||
Backend->>Redis: 清除旧租户的 Token 缓存<br/>DEL sso:access_token:{userId}:8000
|
||||
Backend->>Redis: 缓存新租户的 Access Token<br/>SET sso:access_token:{userId}:6000 {token}<br/>EXPIRE 1800
|
||||
|
||||
Backend->>Redis: 更新 Refresh Token 缓存<br/>SET sso:refresh_token:{userId} {newRefreshToken}<br/>EXPIRE 7200
|
||||
|
||||
Backend->>Redis: 更新用户会话<br/>HSET session:{sessionId}<br/> tenantNo "6000"<br/> accessToken "AT-new-xxx"<br/> refreshToken "RT-new-xxx"
|
||||
|
||||
Backend->>SSO: GET /userinfo<br/>Header: Authorization: Bearer {newAccessToken}
|
||||
SSO-->>Backend: 返回目标租户下的完整用户信息<br/>(包含该租户的部门任职信息)
|
||||
|
||||
Backend->>DB: 更新用户在该租户的信息<br/>UPDATE users SET<br/> position_code = ?,<br/> position_name = ?,<br/> updated_at = NOW()
|
||||
|
||||
Backend->>Backend: 生成新的 SkillHub JWT Token
|
||||
Backend-->>Frontend: 返回切换成功<br/>{<br/> success: true,<br/> data: {<br/> accessToken: "AT-new-xxx",<br/> refreshToken: "RT-new-xxx",<br/> user: {...},<br/> message: "已切换到江苏东方盛虹"<br/> }<br/>}
|
||||
|
||||
Frontend->>Frontend: 更新内存中的 Token
|
||||
Frontend->>Frontend: 更新全局用户状态<br/>(包括租户信息、部门任职等)
|
||||
Frontend->>Frontend: 清除所有 API 查询缓存<br/>(React Query: queryClient.clear())
|
||||
|
||||
Frontend->>User: 显示成功提示<br/>"已切换到江苏东方盛虹"
|
||||
Frontend->>Frontend: 刷新当前页面<br/>window.location.reload()
|
||||
|
||||
User->>Frontend: 页面重新加载
|
||||
Frontend->>Backend: 重新请求数据<br/>(使用新租户的 Token)
|
||||
|
||||
Backend->>DB: 查询该租户的数据<br/>WHERE tenant_no = "6000"
|
||||
Backend-->>Frontend: 返回新租户的数据
|
||||
|
||||
Frontend->>User: 展示新租户的页面内容
|
||||
|
||||
Note over User,Frontend: 租户切换完成<br/>当前租户: 江苏东方盛虹(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 (
|
||||
<Select
|
||||
value={currentTenant}
|
||||
onChange={(tenantNo) => switchTenant(tenantNo)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{tenantList.map(tenant => (
|
||||
<Option key={tenant.tenantNo} value={tenant.tenantNo}>
|
||||
{tenant.tenantName}
|
||||
{tenant.tenantNo === currentTenant && ' (当前)'}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<Project> 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<User>(['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<string>()
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Select
|
||||
value={currentTenant}
|
||||
onChange={handleTenantChange}
|
||||
loading={isLoading}
|
||||
style={{ width: 200 }}
|
||||
placeholder="选择租户"
|
||||
>
|
||||
{tenantList.map(tenant => (
|
||||
<Select.Option key={tenant.tenantNo} value={tenant.tenantNo}>
|
||||
{tenant.tenantName}
|
||||
{tenant.tenantNo === currentTenant && ' ✓'}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Modal
|
||||
title="确认切换租户"
|
||||
open={confirmVisible}
|
||||
onOk={handleConfirm}
|
||||
onCancel={() => setConfirmVisible(false)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>切换租户将刷新页面,未保存的数据可能会丢失。</p>
|
||||
<p>是否继续切换?</p>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 后端实现
|
||||
|
||||
### SsoController - 租户切换接口
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/auth/sso")
|
||||
public class SsoController {
|
||||
|
||||
@Autowired
|
||||
private EnterpriseSsoClient ssoClient;
|
||||
|
||||
@Autowired
|
||||
private IdentityBindingService identityBindingService;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@PostMapping("/switch-tenant")
|
||||
public ResponseEntity<SwitchTenantResponse> switchTenant(
|
||||
@RequestBody SwitchTenantRequest request,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
|
||||
String userId = principal.getUserId();
|
||||
String targetTenantNo = request.getTargetTenantNo();
|
||||
String refreshToken = request.getRefreshToken();
|
||||
|
||||
// 1. 验证用户是否有权访问目标租户
|
||||
List<String> 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<TenantInfo> 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)
|
||||
592
docs/sh-sso/企业SSO系统架构图.md
Normal file
592
docs/sh-sso/企业SSO系统架构图.md
Normal file
|
|
@ -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}<br/>Value: Access Token<br/>TTL: 30分钟"]
|
||||
T2["Key: sso:refresh_token:{userId}<br/>Value: Refresh Token<br/>TTL: 2小时"]
|
||||
end
|
||||
|
||||
subgraph "会话缓存"
|
||||
S1["Key: session:{sessionId}<br/>Value: Session Data<br/>TTL: 2小时"]
|
||||
end
|
||||
|
||||
subgraph "用户信息缓存"
|
||||
U1["Key: user:info:{userId}<br/>Value: User Profile<br/>TTL: 1小时"]
|
||||
U2["Key: user:tenants:{userId}<br/>Value: Tenant List<br/>TTL: 1小时"]
|
||||
end
|
||||
|
||||
subgraph "权限缓存"
|
||||
P1["Key: permissions:{userId}:{tenantNo}<br/>Value: Permission List<br/>TTL: 30分钟"]
|
||||
end
|
||||
|
||||
subgraph "限流缓存"
|
||||
R1["Key: rate:limit:login:{ip}<br/>Value: Counter<br/>TTL: 5分钟"]
|
||||
R2["Key: rate:limit:switch:{userId}<br/>Value: Counter<br/>TTL: 1分钟"]
|
||||
end
|
||||
|
||||
subgraph "防重放缓存"
|
||||
A1["Key: auth:state:{state}<br/>Value: Nonce<br/>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<String> 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<String> 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)
|
||||
226
docs/sh-sso/企业SSO首次登录流程.md
Normal file
226
docs/sh-sso/企业SSO首次登录流程.md
Normal file
|
|
@ -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<br/>{code, state}
|
||||
|
||||
Backend->>Redis: 验证 state 参数
|
||||
alt state 无效或过期
|
||||
Backend-->>Frontend: 返回错误(CSRF 攻击)
|
||||
Frontend->>User: 显示错误,重新登录
|
||||
end
|
||||
|
||||
Backend->>SSO: POST /auth/sso/login<br/>使用 code 换取 Token
|
||||
SSO->>SSO: 验证授权码
|
||||
SSO-->>Backend: 返回 Access Token + Refresh Token + 用户信息
|
||||
|
||||
Backend->>Backend: 解析用户信息
|
||||
Backend->>SSO: GET /userinfo<br/>获取完整用户信息
|
||||
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<br/>{tempToken, newPassword, logoutAfterChange: false}
|
||||
|
||||
Backend->>SSO: POST /password/save-password<br/>修改密码
|
||||
SSO->>SSO: 验证密码强度
|
||||
|
||||
alt 密码不符合要求
|
||||
SSO-->>Backend: 返回错误(密码必须包含大小写字母和数字,长度8-20位)
|
||||
Backend-->>Frontend: 返回错误
|
||||
Frontend->>User: 显示密码要求,重新输入
|
||||
end
|
||||
|
||||
SSO-->>Backend: 密码修改成功
|
||||
Backend->>DB: 创建平台账号
|
||||
Backend->>DB: 创建身份绑定记录<br/>(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<br/>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)
|
||||
Loading…
Add table
Reference in a new issue