QuantumNous / QuantumNous/new-api
目前的项目不支持 sso, 需要支持写入 cookie domain 为顶级域才能完成多项目共用账号体系自动登录的功能。跨域 sso
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 48.5k
- Forks
- 11.6k
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 58
Description
我提供2个参考文件, 我在 https://ai-router.plugins-world.cn 和 https://ai-gateway.plugins-world.cn 做sso登录的时候遇到的问题。提出来看大佬是否集成到仓库中。
SSO_SETUP.md
# SSO 集成配置指南
## 概述
ai-gateway 已集成 New API 的用户认证系统,支持两种模式:
1. **独立登录模式**: 用户在 ai-gateway 独立登录 (当前可用)
2. **SSO 模式**: 与 New API 共享 Session (需要统一域名部署)
## 技术实现
- **Session 存储**: Cookie Store (与 New API 一致)
- **Session 配置**: 与 New API 完全兼容
- Session name: `"session"`
- MaxAge: 30 天 (2592000 秒)
- SameSite: Strict
- HttpOnly: true
- **共享数据库**: 使用相同的 users 表
- **Session 字段**: id, username, role, status, group
## 配置步骤
### 1. 生成 SESSION_SECRET
使用以下命令生成一个随机 UUID:
```bash
python3 -c "import uuid; print(uuid.uuid4())"
# 或
uuidgen
```
示例输出: `b2a044ed-dcd6-4a08-bbb7-f1c034a147b7`
### 2. 配置 New API
编辑 New API 的 `.env.production` 文件:
```bash
# 找到这一行 (大约在 line 62)
# SESSION_SECRET=random_string
# 取消注释并设置为生成的 UUID
SESSION_SECRET=b2a044ed-dcd6-4a08-bbb7-f1c034a147b7
```
**重要**: 如果不配置,New API 每次启动都会生成新的随机值,导致 Session 失效。
### 3. 配置 ai-gateway
编辑 ai-gateway 的配置文件 (例如 `config.production.yaml`):
```yaml
# Session 密钥 (与 New API 保持一致)
SESSION_SECRET: "b2a044ed-dcd6-4a08-bbb7-f1c034a147b7"
```
或者使用环境变量:
```bash
export SESSION_SECRET="b2a044ed-dcd6-4a08-bbb7-f1c034a147b7"
```
### 4. 重启服务
```bash
# 重启 New API
systemctl restart new-api
# 或
docker-compose restart new-api
# 重启 ai-gateway
systemctl restart ai-gateway
# 或
go run cmd/main.go server --config config.production.yaml
```
## 部署架构与 SSO 支持
### ✅ 支持 SSO 的架构
#### 方案 1: 同一域名,不同路径 (推荐)
```
通过 Nginx 反向代理统一域名:
https://example.com/api/* -> New API
https://example.com/gateway/* -> ai-gateway
```
**配置示例** (Nginx):
```nginx
server {
listen 443 ssl;
server_name example.com;
# New API
location /api/ {
proxy_pass http://newapi:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ai-gateway
location /gateway/ {
proxy_pass http://ai-gateway:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
**优点**: Cookie 可以在 `/api` 和 `/gateway` 之间共享
#### 方案 2: 同一域名,不同端口 (本地开发)
```
http://localhost:3000 -> New API
http://localhost:8080 -> ai-gateway
```
**注意**: Cookie 会被共享,但端口不同时需要确保 CORS 配置正确。
### ❌ 不支持 SSO 的架构
#### 情况 1: 不同子域名
```
https://newapi.example.com -> New API
https://gateway.example.com -> ai-gateway
```
**原因**: Cookie 默认只对设置它的域名有效,无法跨子域名共享 (除非修改 New API 代码设置 Domain)。
#### 情况 2: 完全不同的域名
```
https://newapi.com -> New API
https://gateway.com -> ai-gateway
```
**原因**: 浏览器安全限制,Cookie 无法跨域共享。
### 当前使用独立登录模式
如果当前部署在不同域名,系统会使用**独立登录模式**:
- 用户需要分别登录 New API 和 ai-gateway
- 两个系统共享用户数据库,使用相同的账号密码
- 功能完全正常,只是需要登录两次
## 测试步骤
### 测试 1: 独立登录功能
1. 访问 ai-gateway 登录页面: `http://localhost:8080/login`
2. 使用 New API 的管理员账号登录 (role >= 10)
3. 登录成功后应该跳转到 `/gateway/users/stats`
4. 刷新页面,Session 应该保持登录状态
5. 点击右上角退出登录,应该返回登录页面
### 测试 2: 权限控制
1. 尝试访问受保护的路由 (未登录): `http://localhost:8080/gateway/users/stats`
- 应该自动跳转到登录页面
2. 使用非管理员账号登录 (role < 10)
- 应该提示"没有管理员权限"
3. 使用被禁用的账号登录 (status != 1)
- 应该提示"账号已被禁用"
### 测试 3: SSO 功能 (仅当统一域名部署时)
**前提条件**: New API 和 ai-gateway 部署在同一域名
1. 访问 New API 登录页面并登录
2. 在同一浏览器中访问 ai-gateway: `http://example.com/gateway/users/stats`
3. 应该**自动识别 Session**,无需再次登录
4. 在 New API 退出登录
5. 刷新 ai-gateway 页面,应该被跳转到登录页面
### 测试 4: Session 共享验证
使用浏览器开发者工具检查 Cookie:
1. 打开浏览器开发者工具 (F12)
2. 切换到 Application / Storage -> Cookies
3. 检查 Cookie 名称为 `session` 的项
4. 验证:
- Domain: 应该是你的域名
- Path: `/`
- HttpOnly: ✓
- SameSite: Strict
## 故障排查
### 问题 1: 登录后立即退出
**原因**: SESSION_SECRET 配置不一致或未配置
**解决方案**:
1. 确认两个系统的 SESSION_SECRET 完全相同
2. 重启两个服务
3. 清除浏览器 Cookie 后重试
### 问题 2: SSO 不工作 (已登录 New API 但 ai-gateway 仍需登录)
**原因**: 部署在不同域名,Cookie 无法共享
**解决方案**:
- 选项 1: 调整部署架构,统一域名 (使用 Nginx 反向代理)
- 选项 2: 接受独立登录模式
### 问题 3: 提示"没有管理员权限"
**原因**: 当前用户的 role 值小于 10
**解决方案**:
```sql
-- 在数据库中更新用户角色
UPDATE users SET role = 10 WHERE username = 'your_username';
```
### 问题 4: 提示"账号已被禁用"
**原因**: 用户的 status 字段不等于 1
**解决方案**:
```sql
-- 在数据库中启用用户
UPDATE users SET status = 1 WHERE username = 'your_username';
```
## 安全建议
1. **生产环境配置**:
- 设置 `Secure: true` (需要 HTTPS)
- 使用强随机的 SESSION_SECRET
- 定期更换 SESSION_SECRET
2. **密码加密** (TODO):
- 当前使用简单字符串比较
- 生产环境应实现 bcrypt 密码验证
- 参考 New API 的 `model/user.go` 中的 `ValidateAndFill` 方法
3. **CORS 配置**:
- 如果前后端分离部署,需要配置正确的 CORS 策略
- 限制允许的来源域名
## 代码位置参考
- **New API Session 配置**: `/infra-k3s/apps/pluginsworld/new-api/new-api/main.go:143-151`
- **New API 登录逻辑**: `/infra-k3s/apps/pluginsworld/new-api/new-api/controller/user.go:98-105`
- **ai-gateway Session 配置**: `/ai-gateway/cmd/server/cmd.go:70-85`
- **ai-gateway 认证中间件**: `/ai-gateway/internal/middleware/auth.go`
- **ai-gateway 登录处理**: `/ai-gateway/internal/handlers/auth.go`
## 总结
当前实现已完成:
- ✅ 独立登录功能完全可用
- ✅ 权限控制 (管理员角色验证)
- ✅ Session 配置与 New API 兼容
- ✅ 代码架构支持未来启用 SSO (调整部署架构即可)
如需启用 SSO,只需:
1. 调整部署架构到同一域名
2. 配置相同的 SESSION_SECRET
3. 无需修改代码
---
生成时间: 2025-10-23
生成的 SESSION_SECRET 示例: `b2a044ed-dcd6-4a08-bbb7-f1c034a147b7`
sso-cookie-cleanup.md
# SSO Cookie清理方案
## 问题描述
SSO迁移后用户浏览器存在两个同名cookie:
- 旧cookie: `session=xxx; Domain=ai-router.plugins-world.cn`
- 新cookie: `session=xxx; Domain=.plugins-world.cn`
请求发送时两个cookie都会被发送, 导致服务端解析混乱, 用户无法登录.
## 解决方案
服务端通过 `Set-Cookie` 响应头主动清理旧domain的cookie.
## 实现内容
文件: `middleware/cookie-cleanup.go`
自动识别逻辑:
1. 读取 `COOKIE_DOMAIN` 配置 (如 `.plugins-world.cn`)
2. 获取请求 Host (如 `ai-router.plugins-world.cn`)
3. 如果 Host 是 COOKIE_DOMAIN 的子域名, 自动清理 Host 域名下的旧cookie
## 工作原理
1. 用户请求到达服务端, Host = `ai-router.plugins-world.cn`
2. 中间件检测到 `COOKIE_DOMAIN = .plugins-world.cn`
3. 判断 Host 是其子域名, 需要清理
4. 下发响应头: `Set-Cookie: session=; Domain=ai-router.plugins-world.cn; Max-Age=-1`
5. 浏览器删除旧cookie
6. session中间件正常读取新域名下的cookie
## 验证方法
部署后查看响应头, 应包含:
```
Set-Cookie: session=; Domain=ai-router.plugins-world.cn; Max-Age=-1; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly
```
刷新页面后旧cookie被清理, 登录状态恢复正常.
main.go
// Initialize session store
store := cookie.NewStore([]byte(common.SessionSecret))
// 跨域 Cookie 配置 (支持与其他服务共享登录状态)
cookieDomain := os.Getenv("COOKIE_DOMAIN") // 例如: .example.com (注意前面有点)
cookieSecure := os.Getenv("COOKIE_SECURE") == "true" // HTTPS 环境设为 true
// 自动选择 SameSite 模式
sameSiteMode := http.SameSiteStrictMode // 默认严格模式
if cookieDomain != "" {
// 跨子域场景
if cookieSecure {
sameSiteMode = http.SameSiteNoneMode // HTTPS 环境用 None
} else {
sameSiteMode = http.SameSiteLaxMode // HTTP 环境用 Lax
}
common.SysLog(fmt.Sprintf("cookie domain: %s, secure: %v, samesite: %v", cookieDomain, cookieSecure, sameSiteMode))
}
store.Options(sessions.Options{
Path: "/",
Domain: cookieDomain, // 空值表示当前域名,非空表示跨子域
MaxAge: 2592000, // 30 days
HttpOnly: true,
Secure: cookieSecure,
SameSite: sameSiteMode,
})
// 清理旧域名下的cookie(解决SSO迁移后多cookie问题)
server.Use(middleware.CookieCleanup())
server.Use(sessions.Sessions("session", store))
middleware/cookie-cleanup.go
package middleware
import (
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// CookieCleanup 清理旧域名下的cookie
// 用于解决SSO迁移后多个同名cookie导致的登录问题
//
// 自动识别逻辑:
// - 当 COOKIE_DOMAIN 配置为 .plugins-world.cn
// - 请求 Host 为 ai-router.plugins-world.cn
// - 自动清理 ai-router.plugins-world.cn 域名下的旧cookie
//
// 关键修复:
// - 添加 SameSite 属性(与 session 创建时保持一致)
// - 使用环境变量的固定 Secure 值(而非动态判断 TLS)
// - 同时清理父域名和子域名的 cookie
func CookieCleanup() gin.HandlerFunc {
cookieDomain := os.Getenv("COOKIE_DOMAIN")
cookieSecure := os.Getenv("COOKIE_SECURE") == "true"
if cookieDomain == "" {
return func(c *gin.Context) {
c.Next()
}
}
// cookieDomain 格式为 ".plugins-world.cn"
// 去掉前面的点得到 "plugins-world.cn"
baseDomain := strings.TrimPrefix(cookieDomain, ".")
// 计算 SameSite 模式(与 main.go 中的逻辑保持一致)
sameSiteMode := http.SameSiteStrictMode
if cookieDomain != "" {
if cookieSecure {
sameSiteMode = http.SameSiteNoneMode
} else {
sameSiteMode = http.SameSiteLaxMode
}
}
return func(c *gin.Context) {
// 检查请求中是否包含session cookie
if _, err := c.Cookie("session"); err != nil {
c.Next()
return
}
// 获取请求的Host(去掉端口)
host := c.Request.Host
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
// 如果请求Host是baseDomain的子域名,清理多个可能的域名下的cookie
if strings.HasSuffix(host, baseDomain) && host != baseDomain {
// 清理当前子域名下的 cookie
http.SetCookie(c.Writer, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
Domain: host,
MaxAge: -1,
Expires: time.Unix(0, 0),
HttpOnly: true,
Secure: cookieSecure,
SameSite: sameSiteMode,
})
// 清理父域名下的 cookie(确保旧的父域名 cookie 也被清理)
http.SetCookie(c.Writer, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
Domain: cookieDomain,
MaxAge: -1,
Expires: time.Unix(0, 0),
HttpOnly: true,
Secure: cookieSecure,
SameSite: sameSiteMode,
})
}
c.Next()
}
}
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the session setup in main.go and the authentication flow in middleware/cookie-cleanup.go, then compare the referenced New API session configuration and login logic in main.go and controller/user.go. Verify the configured domain, SameSite, Secure, and cleanup behavior using the issue's browser-cookie checks; done means shared login works across the documented subdomains without duplicate session cookies.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100