QuantumNous / QuantumNous/new-api

目前的项目不支持 sso, 需要支持写入 cookie domain 为顶级域才能完成多项目共用账号体系自动登录的功能。跨域 sso

Open
#2,248 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement stale
Dominant language
Go
Stars
48.5k
Forks
11.6k
Avg merge
1d 17h
Merged PRs (30d)
58

Description

我提供2个参考文件, 我在 https://ai-router.plugins-world.cnhttps://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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.