From 39593052b67ca186c8623f3173b1339a88f88716 Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 15 Dec 2025 17:24:09 +0800 Subject: [PATCH] feat(auth): enhance IP restriction handling with CIDR support --- common/ip.go | 29 +++++++++++++++++++ common/ssrf_protection.go | 18 +----------- common/utils.go | 5 ---- middleware/auth.go | 15 ++++++++-- model/token.go | 15 +++++----- .../table/tokens/modals/EditTokenModal.jsx | 4 +-- web/src/i18n/locales/en.json | 4 +-- web/src/i18n/locales/fr.json | 4 +-- web/src/i18n/locales/ja.json | 4 +-- web/src/i18n/locales/ru.json | 4 +-- web/src/i18n/locales/vi.json | 4 +-- web/src/i18n/locales/zh.json | 4 +-- 12 files changed, 63 insertions(+), 47 deletions(-) diff --git a/common/ip.go b/common/ip.go index bfb64ee7f..0f2a41ffd 100644 --- a/common/ip.go +++ b/common/ip.go @@ -2,6 +2,15 @@ package common import "net" +func IsIP(s string) bool { + ip := net.ParseIP(s) + return ip != nil +} + +func ParseIP(s string) net.IP { + return net.ParseIP(s) +} + func IsPrivateIP(ip net.IP) bool { if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return true @@ -20,3 +29,23 @@ func IsPrivateIP(ip net.IP) bool { } return false } + +func IsIpInCIDRList(ip net.IP, cidrList []string) bool { + for _, cidr := range cidrList { + _, network, err := net.ParseCIDR(cidr) + if err != nil { + // 尝试作为单个IP处理 + if whitelistIP := net.ParseIP(cidr); whitelistIP != nil { + if ip.Equal(whitelistIP) { + return true + } + } + continue + } + + if network.Contains(ip) { + return true + } + } + return false +} diff --git a/common/ssrf_protection.go b/common/ssrf_protection.go index 6f7d289f1..3cd5c2ea1 100644 --- a/common/ssrf_protection.go +++ b/common/ssrf_protection.go @@ -186,23 +186,7 @@ func isIPListed(ip net.IP, list []string) bool { return false } - for _, whitelistCIDR := range list { - _, network, err := net.ParseCIDR(whitelistCIDR) - if err != nil { - // 尝试作为单个IP处理 - if whitelistIP := net.ParseIP(whitelistCIDR); whitelistIP != nil { - if ip.Equal(whitelistIP) { - return true - } - } - continue - } - - if network.Contains(ip) { - return true - } - } - return false + return IsIpInCIDRList(ip, list) } // IsIPAccessAllowed 检查IP是否允许访问 diff --git a/common/utils.go b/common/utils.go index 0ffa128e7..f63df857b 100644 --- a/common/utils.go +++ b/common/utils.go @@ -217,11 +217,6 @@ func IntMax(a int, b int) int { } } -func IsIP(s string) bool { - ip := net.ParseIP(s) - return ip != nil -} - func GetUUID() string { code := uuid.New().String() code = strings.Replace(code, "-", "", -1) diff --git a/middleware/auth.go b/middleware/auth.go index d24120042..9bc2f0428 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -2,12 +2,14 @@ package middleware import ( "fmt" + "net" "net/http" "strconv" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/ratio_setting" @@ -240,13 +242,20 @@ func TokenAuth() func(c *gin.Context) { return } - allowIpsMap := token.GetIpLimitsMap() - if len(allowIpsMap) != 0 { + allowIpsMap := token.GetIpLimits() + if len(allowIpsMap) > 0 { clientIp := c.ClientIP() - if _, ok := allowIpsMap[clientIp]; !ok { + logger.LogDebug(c, "Token has IP restrictions, checking client IP %s", clientIp) + ip := net.ParseIP(clientIp) + if ip == nil { + abortWithOpenAiMessage(c, http.StatusForbidden, "无法解析客户端 IP 地址") + return + } + if common.IsIpInCIDRList(ip, allowIpsMap) == false { abortWithOpenAiMessage(c, http.StatusForbidden, "您的 IP 不在令牌允许访问的列表中") return } + logger.LogDebug(c, "Client IP %s passed the token IP restrictions check", clientIp) } userCache, err := model.GetUserCache(token.UserId) diff --git a/model/token.go b/model/token.go index a6a307ac2..357d9bdd2 100644 --- a/model/token.go +++ b/model/token.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/bytedance/gopkg/util/gopool" "gorm.io/gorm" ) @@ -35,26 +34,26 @@ func (token *Token) Clean() { token.Key = "" } -func (token *Token) GetIpLimitsMap() map[string]any { +func (token *Token) GetIpLimits() []string { // delete empty spaces //split with \n - ipLimitsMap := make(map[string]any) + ipLimits := make([]string, 0) if token.AllowIps == nil { - return ipLimitsMap + return ipLimits } cleanIps := strings.ReplaceAll(*token.AllowIps, " ", "") if cleanIps == "" { - return ipLimitsMap + return ipLimits } ips := strings.Split(cleanIps, "\n") for _, ip := range ips { ip = strings.TrimSpace(ip) ip = strings.ReplaceAll(ip, ",", "") - if common.IsIP(ip) { - ipLimitsMap[ip] = true + if ip != "" { + ipLimits = append(ipLimits, ip) } } - return ipLimitsMap + return ipLimits } func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { diff --git a/web/src/components/table/tokens/modals/EditTokenModal.jsx b/web/src/components/table/tokens/modals/EditTokenModal.jsx index c7db40d66..cc9f51b0e 100644 --- a/web/src/components/table/tokens/modals/EditTokenModal.jsx +++ b/web/src/components/table/tokens/modals/EditTokenModal.jsx @@ -557,11 +557,11 @@ const EditTokenModal = (props) => { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 448559f82..188f9e693 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -97,7 +97,7 @@ "Homepage URL 填": "Fill in the Homepage URL", "ID": "ID", "IP": "IP", - "IP白名单": "IP whitelist", + "IP白名单(支持CIDR表达式)": "IP whitelist (supports CIDR expressions)", "IP限制": "IP restrictions", "IP黑名单": "IP blacklist", "JSON": "JSON", @@ -1752,7 +1752,7 @@ "请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first", "请再次输入新密码": "Please enter the new password again", "请前往个人设置 → 安全设置进行配置。": "Please go to Personal Settings → Security Settings to configure.", - "请勿过度信任此功能,IP可能被伪造": "Do not over-trust this feature, IP can be spoofed", + "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用": "Do not over-trust this feature, IP can be spoofed, please use it in conjunction with gateways such as nginx and CDN", "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:", "请填写完整的产品信息": "Please fill in complete product information", "请填写完整的管理员账号信息": "Please fill in the complete administrator account information", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index a53d459c9..b314f8608 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -99,7 +99,7 @@ "Homepage URL 填": "Remplir l'URL de la page d'accueil", "ID": "ID", "IP": "IP", - "IP白名单": "Liste blanche d'adresses IP", + "IP白名单(支持CIDR表达式)": "Liste blanche d'adresses IP (prise en charge des expressions CIDR)", "IP限制": "Restrictions d'IP", "IP黑名单": "Liste noire d'adresses IP", "JSON": "JSON", @@ -1762,7 +1762,7 @@ "请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité", "请再次输入新密码": "Veuillez saisir à nouveau le nouveau mot de passe", "请前往个人设置 → 安全设置进行配置。": "Veuillez aller dans Paramètres personnels → Paramètres de sécurité pour configurer.", - "请勿过度信任此功能,IP可能被伪造": "Ne faites pas trop confiance à cette fonctionnalité, l'IP peut être usurpée", + "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用": "Ne faites pas trop confiance à cette fonctionnalité, l'IP peut être usurpée, veuillez l'utiliser en conjonction avec des passerelles telles que nginx et cdn", "请在系统设置页面编辑分组倍率以添加新的分组:": "Veuillez modifier les ratios de groupe dans les paramètres système pour ajouter de nouveaux groupes :", "请填写完整的产品信息": "Veuillez renseigner l'ensemble des informations produit", "请填写完整的管理员账号信息": "Veuillez remplir les informations complètes du compte administrateur", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 72d17e0b9..b5767f662 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -82,7 +82,7 @@ "Homepage URL 填": "ホームページURLを入力してください", "ID": "ID", "IP": "IP", - "IP白名单": "IPホワイトリスト", + "IP白名单(支持CIDR表达式)": "IPホワイトリスト(CIDR表記に対応)", "IP限制": "IP制限", "IP黑名单": "IPブラックリスト", "JSON": "JSON", @@ -1669,7 +1669,7 @@ "请先阅读并同意用户协议和隐私政策": "まずユーザー利用規約とプライバシーポリシーをご確認の上、同意してください", "请再次输入新密码": "新しいパスワードを再入力してください", "请前往个人设置 → 安全设置进行配置。": "アカウント設定 → セキュリティ設定 にて設定してください。", - "请勿过度信任此功能,IP可能被伪造": "IPは偽装される可能性があるため、この機能を過信しないでください", + "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用": "IPは偽装される可能性があるため、この機能を過信しないでください。nginxやCDNなどのゲートウェイと組み合わせて使用してください。", "请在系统设置页面编辑分组倍率以添加新的分组:": "新規グループを追加するには、システム設定ページでグループ倍率を編集してください:", "请填写完整的管理员账号信息": "管理者アカウント情報をすべて入力してください", "请填写密钥": "APIキーを入力してください", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 85659818e..046a84bff 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -101,7 +101,7 @@ "Homepage URL 填": "URL домашней страницы:", "ID": "ID", "IP": "IP", - "IP白名单": "Белый список IP", + "IP白名单(支持CIDR表达式)": "Белый список IP (поддерживает выражения CIDR)", "IP限制": "Ограничения IP", "IP黑名单": "Черный список IP", "JSON": "JSON", @@ -1773,7 +1773,7 @@ "请先阅读并同意用户协议和隐私政策": "Пожалуйста, сначала прочтите и согласитесь с пользовательским соглашением и политикой конфиденциальности", "请再次输入新密码": "Пожалуйста, введите новый пароль ещё раз", "请前往个人设置 → 安全设置进行配置。": "Пожалуйста, перейдите в Личные настройки → Настройки безопасности для конфигурации.", - "请勿过度信任此功能,IP可能被伪造": "Не доверяйте этой функции чрезмерно, IP может быть подделан", + "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用": "Не доверяйте этой функции чрезмерно, IP может быть подделан, используйте её вместе с nginx и CDN и другими шлюзами", "请在系统设置页面编辑分组倍率以添加新的分组:": "Пожалуйста, отредактируйте коэффициенты групп на странице системных настроек для добавления новой группы:", "请填写完整的产品信息": "Пожалуйста, заполните всю информацию о продукте", "请填写完整的管理员账号信息": "Пожалуйста, заполните полную информацию об учётной записи администратора", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 6e8076c56..669cafec6 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -82,7 +82,7 @@ "Homepage URL 填": "Điền URL trang chủ", "ID": "ID", "IP": "IP", - "IP白名单": "Danh sách trắng IP", + "IP白名单(支持CIDR表达式)": "Danh sách trắng IP (hỗ trợ biểu thức CIDR)", "IP限制": "Hạn chế IP", "IP黑名单": "Danh sách đen IP", "JSON": "JSON", @@ -1987,7 +1987,7 @@ "请先阅读并同意用户协议和隐私政策": "Vui lòng đọc và đồng ý với thỏa thuận người dùng và chính sách bảo mật trước", "请再次输入新密码": "Vui lòng nhập lại mật khẩu mới", "请前往个人设置 → 安全设置进行配置。": "Vui lòng truy cập Cài đặt cá nhân → Cài đặt bảo mật để cấu hình.", - "请勿过度信任此功能,IP可能被伪造": "Đừng quá tin tưởng tính năng này, IP có thể bị giả mạo", + "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用": "Đừng quá tin tưởng tính năng này, IP có thể bị giả mạo, vui lòng sử dụng cùng với nginx và các cổng khác như cdn", "请在系统设置页面编辑分组倍率以添加新的分组:": "Vui lòng chỉnh sửa tỷ lệ nhóm trên trang cài đặt hệ thống để thêm nhóm mới:", "请填写完整的管理员账号信息": "Vui lòng điền đầy đủ thông tin tài khoản quản trị viên", "请填写密钥": "Vui lòng điền khóa", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 273cc24f2..304a13b03 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -95,7 +95,7 @@ "Homepage URL 填": "Homepage URL 填", "ID": "ID", "IP": "IP", - "IP白名单": "IP白名单", + "IP白名单(支持CIDR表达式)": "IP白名单(支持CIDR表达式)", "IP限制": "IP限制", "IP黑名单": "IP黑名单", "JSON": "JSON", @@ -1740,7 +1740,7 @@ "请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策", "请再次输入新密码": "请再次输入新密码", "请前往个人设置 → 安全设置进行配置。": "请前往个人设置 → 安全设置进行配置。", - "请勿过度信任此功能,IP可能被伪造": "请勿过度信任此功能,IP可能被伪造", + "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用": "请勿过度信任此功能,IP可能被伪造,请配合nginx和cdn等网关使用", "请在系统设置页面编辑分组倍率以添加新的分组:": "请在系统设置页面编辑分组倍率以添加新的分组:", "请填写完整的产品信息": "请填写完整的产品信息", "请填写完整的管理员账号信息": "请填写完整的管理员账号信息",