131 lines
5.4 KiB
Markdown
131 lines
5.4 KiB
Markdown
# 单台服务器多端口 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/zf9wx4xm7 登录
|
||
6. 自动跳转回 :5173,进入后台
|
||
7. 打开 `http://129.211.173.87:9000/api/userinfo`,应返回已登录用户信息
|
||
8. 调用 `http://129.211.173.87:9000/api/logout`,再访问 blog 应返回 401
|