This commit is contained in:
Ubuntu
2026-07-03 16:58:28 +08:00
commit 32b43f85bd
6240 changed files with 1987678 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
# 单台服务器多端口 SSO 方案
## 场景约束
- 单台服务器,公网 IP `129.211.173.87`,无域名
- 所有系统通过 `IP:端口` 访问
- 已安装 Redis,不需要 HTTPS(纯 HTTP)
- 目标项目:`personal-blog`(FastAPI 后端 + React 前端)
## 核心原理
同 IP 下不同端口属于 **same-site**,浏览器会自动携带该 IP 下的所有 Cookie。
因此:
- SSO 中心登录后种下 Cookie(Domain 设为 IP 地址)
- 各业务系统同 IP 不同端口均可读取该 Cookie
- 无需 OAuth2 跳转,无需重定向
> 不同端口之间发 AJAX 仍属于 **cross-origin**,需要 CORS 配置 `allow_credentials=True`。
## 架构
```
┌─────────────────────────────────────────────────────────┐
│ 129.211.173.87 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 业务前端 │ │ 业务后端 │ │ SSO中心 │ │
│ │ :5173 │ │ :8889 │ │ :9000 │ │
│ │ React+Vite │ │ FastAPI │ │ FastAPI │ │
│ │ axios │ │ 验签Cookie │ │ 签发JWT │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ 共享 IP 的 Cookie │
└─────────────────────────────────────────────────────────┘
```
## 数据流
### 登录
```
1. 用户访问 blog 前端 (:5173),未登录,跳转 SSO 登录页 (:9000)
2. SSO 登录页提交用户名密码到 :9000/api/login
3. 验证通过后,SSO 种下两个 Cookie:
- access_token (JWT, 15分钟)
- refresh_token (随机串, Redis 存 7天)
4. 跳转回 blog 前端 (:5173)
5. blog 前端 axios 请求 blog 后端,自动携带 Cookie
6. blog 后端从 Cookie 读 access_token,本地验签,返回数据
```
### 登出
```
1. 前端调用 :9000/api/logout
2. SSO 清除 Cookie,Redis 删除 refresh_token
3. 业务系统再请求时 Cookie 已空,返回 401
```
## Cookie 设计
| 属性 | 值 | 说明 |
|------|-----|------|
| Name | `access_token` / `refresh_token` | |
| Domain | `129.211.173.87` | 同 IP 共享 |
| Path | `/` | 全站可用 |
| HttpOnly | `true` | 防 XSS |
| SameSite | `Lax` | 同 IP 不同端口算 same-site,Lax 足够;HTTP 下不能用 None+Secure |
| MaxAge | 900s / 604800s | 15分钟 / 7天 |
## SSO 中心实现清单
- `app.py` — FastAPI 主应用,端口 9000
- `database.py` — SQLite 用户库
- `models.py` — User 模型
- `sso_config.py` — SECRET_KEY / Redis 配置
- `static/login.html` — 独立登录页
- `requirements.txt`
接口:
- `POST /api/login` — 登录,种 Cookie
- `POST /api/logout` — 登出,清 Cookie
- `GET /api/userinfo` — 读 Cookie 返回当前用户
- `GET /api/validate` — 读 Cookie 返回验证结果(供业务系统可选调用)
- `GET /` — 返回登录页
## 业务系统接入修改(personal-blog)
**原则:改最小,不动原有登录**
### 后端(`backend/`)
1. **新增 `sso_client.py`**
- 提供 `get_current_user_from_cookie()` 函数
- 从请求 Cookie 读 `access_token`
- 用 SSO 相同的 SECRET_KEY 本地验签(HS256)
- 返回 `{"username": "..."}`
2. **修改 `routers/auth.py`**
- `get_current_admin` 改为:
- 先尝试原有 `OAuth2PasswordBearer` Header(兼容旧本地登录)
- Header 不存在则尝试 Cookie 中的 SSO token
- SSO token 验签通过后,用 username 查本地 Admin 表
- 查到则返回,查不到则 401
3. **修改 `app.py` CORS**
- `allow_origins` 加入 `http://129.211.173.87:9000`(SSO中心)
- `allow_credentials=True` 保持开启
### 前端(`frontend/src/`)
1. **修改 `api/config.js`**
- axios 增加 `withCredentials: true`(让跨域请求携带 Cookie)
- CORS 请求必须带 Cookie,否则 SSO 种下的 Cookie 无法被后续请求使用
2. **修改 `pages/Login.jsx`**
- 保留现有本地登录表单
- 新增按钮【SSO 统一登录】,点击跳转 `http://129.211.173.87:9000/?redirect=http://129.211.173.87:5173`
- 页面加载时:如果 localStorage 没有 token,先调用 `authApi.getMe()` 试探(后端可能通过 Cookie 已识别用户),若成功则自动进入后台
## 验证步骤
1. 启动 SSO 中心:`python usercenter/app.py` → 监听 :9000
2. 启动 blog 后端:`python personal-blog/backend/app.py` → 监听 :8889
3. 启动 blog 前端:`cd personal-blog/frontend && npm run dev` → :5173
4. 浏览器访问 `http://129.211.173.87:5173`
5. 点击【SSO 统一登录】,在 :9000 输入 admin/admin123 登录
6. 自动跳转回 :5173,进入后台
7. 打开 `http://129.211.173.87:9000/api/userinfo`,应返回已登录用户信息
8. 调用 `http://129.211.173.87:9000/api/logout`,再访问 blog 应返回 401