5.4 KiB
5.4 KiB
单台服务器多端口 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 主应用,端口 9000database.py— SQLite 用户库models.py— User 模型sso_config.py— SECRET_KEY / Redis 配置static/login.html— 独立登录页requirements.txt
接口:
POST /api/login— 登录,种 CookiePOST /api/logout— 登出,清 CookieGET /api/userinfo— 读 Cookie 返回当前用户GET /api/validate— 读 Cookie 返回验证结果(供业务系统可选调用)GET /— 返回登录页
业务系统接入修改(personal-blog)
原则:改最小,不动原有登录
后端(backend/)
-
新增
sso_client.py- 提供
get_current_user_from_cookie()函数 - 从请求 Cookie 读
access_token - 用 SSO 相同的 SECRET_KEY 本地验签(HS256)
- 返回
{"username": "..."}
- 提供
-
修改
routers/auth.pyget_current_admin改为:- 先尝试原有
OAuth2PasswordBearerHeader(兼容旧本地登录) - Header 不存在则尝试 Cookie 中的 SSO token
- SSO token 验签通过后,用 username 查本地 Admin 表
- 查到则返回,查不到则 401
- 先尝试原有
-
修改
app.pyCORSallow_origins加入http://129.211.173.87:9000(SSO中心)allow_credentials=True保持开启
前端(frontend/src/)
-
修改
api/config.js- axios 增加
withCredentials: true(让跨域请求携带 Cookie) - CORS 请求必须带 Cookie,否则 SSO 种下的 Cookie 无法被后续请求使用
- axios 增加
-
修改
pages/Login.jsx- 保留现有本地登录表单
- 新增按钮【SSO 统一登录】,点击跳转
http://129.211.173.87:9000/?redirect=http://129.211.173.87:5173 - 页面加载时:如果 localStorage 没有 token,先调用
authApi.getMe()试探(后端可能通过 Cookie 已识别用户),若成功则自动进入后台
验证步骤
- 启动 SSO 中心:
python usercenter/app.py→ 监听 :9000 - 启动 blog 后端:
python personal-blog/backend/app.py→ 监听 :8889 - 启动 blog 前端:
cd personal-blog/frontend && npm run dev→ :5173 - 浏览器访问
http://129.211.173.87:5173 - 点击【SSO 统一登录】,在 :9000 输入 admin/zf9wx4xm7 登录
- 自动跳转回 :5173,进入后台
- 打开
http://129.211.173.87:9000/api/userinfo,应返回已登录用户信息 - 调用
http://129.211.173.87:9000/api/logout,再访问 blog 应返回 401