# 单台服务器多端口 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