Files
22ee0aa112 修复安全问题 (#9)
* fix:修复权限问题

* fix:修复项目key类型错误问题

* fix:修复rbac权限问题

* fix:修复api密钥未及时失效的问题

* improve:优化渠道调度稳定性

* improve:优化路由

* fix:修复项目预算控制缺陷

* fix:加强限流等安全措施

* feat:添加限流相关字段的前端展示及接口字段

* improve:优化注册时密码要求,只需要字母和数字即可

* feat(setting):添加注册限流设置

* improve:优化前端数字类型转换

* chore:rbac权限放行

---------

Co-authored-by: chengqian <wuchengqian@kkidc.com>
Co-authored-by: qian <wuchengqian5@163.com>
2026-06-23 22:47:54 +08:00

110 lines
2.4 KiB
Go

package billing
import (
"net"
"strings"
)
// Scope constants
const (
ScopeFull = "full"
ScopeChatOnly = "chat_only"
ScopeEmbeddingsOnly = "embeddings_only"
ScopeImagesOnly = "images_only"
ScopeAudioOnly = "audio_only"
ScopeReadOnly = "read_only"
)
// CheckScope 检查 API Key scope 是否允许访问指定 relay mode
func CheckScope(scope string, relayMode string) bool {
if scope == "" || scope == ScopeFull {
return true
}
switch scope {
case ScopeReadOnly:
// read_only 仅允许 GET /models
return relayMode == ""
case ScopeChatOnly, "chat":
return relayMode == "chat_completions" ||
relayMode == "completions" ||
relayMode == "responses" ||
relayMode == "claude_messages" ||
relayMode == "gemini_generate_content" ||
relayMode == "realtime"
case ScopeEmbeddingsOnly, "embedding":
return relayMode == "embeddings"
case ScopeImagesOnly, "image":
return relayMode == "images_generations" || relayMode == "images_edits"
case ScopeAudioOnly, "audio":
return relayMode == "audio" ||
relayMode == "tts" ||
relayMode == "stt" ||
relayMode == "audio_speech" ||
relayMode == "audio_transcriptions" ||
relayMode == "audio_translations"
default:
// custom scope: 以逗号分隔的模式列表
allowed := strings.Split(scope, ",")
for _, a := range allowed {
if strings.TrimSpace(a) == relayMode {
return true
}
}
return false
}
}
// IsReadOnlyScope 检查 scope 是否为只读
func IsReadOnlyScope(scope string) bool {
return scope == ScopeReadOnly
}
// CheckIPWhitelist 检查 IP 白名单
// whitelist 为逗号分隔的 IP/CIDR 列表,空列表表示不限制
func CheckIPWhitelist(whitelist string, clientIP string) bool {
if whitelist == "" {
return true
}
// 提取 IP(去除端口)
host, _, err := net.SplitHostPort(clientIP)
if err != nil {
// 没有端口,直接使用原始值
host = clientIP
}
// 去除 IPv6 方括号
host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
parsedIP := net.ParseIP(host)
if parsedIP == nil {
return false
}
allowed := strings.Split(whitelist, ",")
for _, a := range allowed {
a = strings.TrimSpace(a)
if a == "" {
continue
}
// 精确 IP 匹配
if a == host {
return true
}
// CIDR 匹配
if strings.Contains(a, "/") {
_, cidr, err := net.ParseCIDR(a)
if err != nil {
continue
}
if cidr.Contains(parsedIP) {
return true
}
}
}
return false
}