feat: 十轮网关优化 - 安全加固/可观测性/性能/可靠性
- SSE Keepalive Ping (15s心跳防止代理断连) - Timing HTTP 头 (X-Timing-Queue/Inference/Total-Ms) - Adapter Request-ID 传播到后端 - Session 清理日志回调 - Server 安全加固 (ReadHeaderTimeout/MaxHeaderBytes 防 slowloris) - Usage Tracker 数据保留清理 (retentionDays + 定期清理) - Config Reload 后 Adapter Registry 更新 (RegisterIfAbsent + RWMutex) - Rate Limiter 空闲 Bucket 清理 (30分钟过期) - Shutdown Drain 超时可配置 (ShutdownDrainSeconds) - Config 模型字段校验增强 (provider/endpoint/actual_model) - Auth 过期 Key 自动清理 (5分钟扫描) - Admin API Rate Limiting - Adapter Health Check 独立超时 (每个 adapter 3s) - TCP 连接阶段超时 (DialContext 5s + KeepAlive 30s) - 幂等键缓存、审计日志、Gzip 中间件、CORS Expose Headers - Backpressure 响应头、熔断器 Prometheus 指标 - 连接池优化、Trace-ID 全链路传播
This commit is contained in:
+220
-7
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/edgeai/gateway/internal/handler"
|
||||
"github.com/edgeai/gateway/internal/middleware"
|
||||
@@ -25,6 +27,7 @@ type AppIdentity struct {
|
||||
AllowedModels []string
|
||||
AllowedPriorities []int
|
||||
IsAdmin bool
|
||||
ExpiresAt *time.Time // 过期时间,nil 表示永不过期
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
@@ -39,6 +42,7 @@ type Authenticator struct {
|
||||
keys map[string]*AppIdentity // hashed_key -> identity
|
||||
db *sql.DB
|
||||
logger *observability.Logger
|
||||
stopCh chan struct{} // 停止清理 goroutine
|
||||
}
|
||||
|
||||
// NewAuthenticator creates a new Authenticator with SQLite storage.
|
||||
@@ -56,12 +60,15 @@ func NewAuthenticator(dbPath string, logger *observability.Logger) (*Authenticat
|
||||
keys: make(map[string]*AppIdentity),
|
||||
db: db,
|
||||
logger: logger,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
if err := a.loadKeys(); err != nil {
|
||||
return nil, fmt.Errorf("load api keys: %w", err)
|
||||
}
|
||||
|
||||
go a.cleanupExpiredKeys()
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@@ -85,7 +92,7 @@ func initAuthDB(db *sql.DB) error {
|
||||
}
|
||||
|
||||
func (a *Authenticator) loadKeys() error {
|
||||
rows, err := a.db.Query(`SELECT key_hash, app_id, tenant_id, name, allowed_models, allowed_priorities, is_admin FROM api_keys WHERE enabled = 1`)
|
||||
rows, err := a.db.Query(`SELECT key_hash, app_id, tenant_id, name, allowed_models, allowed_priorities, is_admin, expires_at FROM api_keys WHERE enabled = 1`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,7 +101,8 @@ func (a *Authenticator) loadKeys() error {
|
||||
for rows.Next() {
|
||||
var hash, appID, tenantID, name, allowedModelsJSON, allowedPrioritiesJSON string
|
||||
var isAdmin int
|
||||
if err := rows.Scan(&hash, &appID, &tenantID, &name, &allowedModelsJSON, &allowedPrioritiesJSON, &isAdmin); err != nil {
|
||||
var expiresAtStr sql.NullString
|
||||
if err := rows.Scan(&hash, &appID, &tenantID, &name, &allowedModelsJSON, &allowedPrioritiesJSON, &isAdmin, &expiresAtStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -104,6 +112,11 @@ func (a *Authenticator) loadKeys() error {
|
||||
Name: name,
|
||||
IsAdmin: isAdmin == 1,
|
||||
}
|
||||
if expiresAtStr.Valid && expiresAtStr.String != "" {
|
||||
if t, err := time.Parse(time.RFC3339, expiresAtStr.String); err == nil {
|
||||
identity.ExpiresAt = &t
|
||||
}
|
||||
}
|
||||
if allowedModelsJSON != "" && allowedModelsJSON != "null" {
|
||||
json.Unmarshal([]byte(allowedModelsJSON), &identity.AllowedModels)
|
||||
}
|
||||
@@ -116,6 +129,13 @@ func (a *Authenticator) loadKeys() error {
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// GenerateAPIKey 生成一个随机的 API Key(前缀 edgeai- + 32 字节随机十六进制)。
|
||||
func GenerateAPIKey() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
return "edgeai-" + hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// hashKey hashes an API key with SHA-256.
|
||||
func hashKey(key string) string {
|
||||
h := sha256.Sum256([]byte(key))
|
||||
@@ -123,6 +143,7 @@ func hashKey(key string) string {
|
||||
}
|
||||
|
||||
// Authenticate validates an API key and returns the AppIdentity.
|
||||
// 检查 Key 是否存在且未过期。
|
||||
func (a *Authenticator) Authenticate(apiKey string) (*AppIdentity, bool) {
|
||||
hash := hashKey(apiKey)
|
||||
a.mu.RLock()
|
||||
@@ -131,19 +152,29 @@ func (a *Authenticator) Authenticate(apiKey string) (*AppIdentity, bool) {
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
// 检查过期
|
||||
if identity.ExpiresAt != nil && time.Now().After(*identity.ExpiresAt) {
|
||||
return nil, false
|
||||
}
|
||||
return identity, true
|
||||
}
|
||||
|
||||
// AddKey adds a new API key (for management API).
|
||||
// 如果 identity.ExpiresAt 不为 nil,则设置过期时间。
|
||||
func (a *Authenticator) AddKey(apiKey string, identity *AppIdentity) error {
|
||||
hash := hashKey(apiKey)
|
||||
allowedModelsJSON, _ := json.Marshal(identity.AllowedModels)
|
||||
allowedPrioritiesJSON, _ := json.Marshal(identity.AllowedPriorities)
|
||||
|
||||
var expiresAt interface{}
|
||||
if identity.ExpiresAt != nil {
|
||||
expiresAt = identity.ExpiresAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
_, err := a.db.Exec(
|
||||
`INSERT INTO api_keys (app_id, tenant_id, name, key_hash, allowed_models, allowed_priorities, is_admin, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
identity.AppID, identity.TenantID, identity.Name, hash, string(allowedModelsJSON), string(allowedPrioritiesJSON), isAdminInt(identity.IsAdmin),
|
||||
`INSERT INTO api_keys (app_id, tenant_id, name, key_hash, allowed_models, allowed_priorities, is_admin, enabled, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
||||
identity.AppID, identity.TenantID, identity.Name, hash, string(allowedModelsJSON), string(allowedPrioritiesJSON), isAdminInt(identity.IsAdmin), expiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -155,6 +186,164 @@ func (a *Authenticator) AddKey(apiKey string, identity *AppIdentity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyInfo 是 API Key 的元信息(不含哈希值)。
|
||||
// 用于管理 API 列出所有 Key。
|
||||
type KeyInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
AppID string `json:"app_id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
AllowedModels []string `json:"allowed_models"`
|
||||
AllowedPriorities []int `json:"allowed_priorities"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// ListKeys 列出所有 API Key 的元信息(不含哈希值)。
|
||||
func (a *Authenticator) ListKeys() ([]KeyInfo, error) {
|
||||
rows, err := a.db.Query(
|
||||
`SELECT id, app_id, tenant_id, name, allowed_models, allowed_priorities, is_admin, enabled, created_at, expires_at FROM api_keys ORDER BY id DESC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query api keys: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var keys []KeyInfo
|
||||
for rows.Next() {
|
||||
var ki KeyInfo
|
||||
var allowedModelsJSON, allowedPrioritiesJSON string
|
||||
var isAdmin, enabled int
|
||||
var expiresAt sql.NullString
|
||||
if err := rows.Scan(&ki.ID, &ki.AppID, &ki.TenantID, &ki.Name, &allowedModelsJSON, &allowedPrioritiesJSON, &isAdmin, &enabled, &ki.CreatedAt, &expiresAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ki.IsAdmin = isAdmin == 1
|
||||
ki.Enabled = enabled == 1
|
||||
if expiresAt.Valid && expiresAt.String != "" {
|
||||
ki.ExpiresAt = expiresAt.String
|
||||
}
|
||||
if allowedModelsJSON != "" && allowedModelsJSON != "null" {
|
||||
json.Unmarshal([]byte(allowedModelsJSON), &ki.AllowedModels)
|
||||
}
|
||||
if allowedPrioritiesJSON != "" && allowedPrioritiesJSON != "null" {
|
||||
json.Unmarshal([]byte(allowedPrioritiesJSON), &ki.AllowedPriorities)
|
||||
}
|
||||
keys = append(keys, ki)
|
||||
}
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
// DisableKey 禁用一个 API Key(通过数据库 ID)。
|
||||
func (a *Authenticator) DisableKey(id int64) error {
|
||||
// 先查出 hash 以便从内存中移除
|
||||
var hash string
|
||||
err := a.db.QueryRow(`SELECT key_hash FROM api_keys WHERE id = ?`, id).Scan(&hash)
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("api key not found: %d", id)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("query api key: %w", err)
|
||||
}
|
||||
|
||||
_, err = a.db.Exec(`UPDATE api_keys SET enabled = 0 WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("disable api key: %w", err)
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
delete(a.keys, hash)
|
||||
a.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteKey 彻底删除一个 API Key(通过数据库 ID)。
|
||||
func (a *Authenticator) DeleteKey(id int64) error {
|
||||
var hash string
|
||||
err := a.db.QueryRow(`SELECT key_hash FROM api_keys WHERE id = ?`, id).Scan(&hash)
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("api key not found: %d", id)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("query api key: %w", err)
|
||||
}
|
||||
|
||||
_, err = a.db.Exec(`DELETE FROM api_keys WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete api key: %w", err)
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
delete(a.keys, hash)
|
||||
a.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RotateKey 轮换 API Key:生成新 Key,禁用旧 Key,返回新 Key 值。
|
||||
func (a *Authenticator) RotateKey(id int64) (string, error) {
|
||||
var oldHash string
|
||||
var appID, tenantID, name, allowedModelsJSON, allowedPrioritiesJSON string
|
||||
var isAdmin int
|
||||
var expiresAt sql.NullString
|
||||
err := a.db.QueryRow(
|
||||
`SELECT key_hash, app_id, tenant_id, name, allowed_models, allowed_priorities, is_admin, expires_at FROM api_keys WHERE id = ?`,
|
||||
id,
|
||||
).Scan(&oldHash, &appID, &tenantID, &name, &allowedModelsJSON, &allowedPrioritiesJSON, &isAdmin, &expiresAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", fmt.Errorf("api key not found: %d", id)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("query api key: %w", err)
|
||||
}
|
||||
|
||||
newKey := GenerateAPIKey()
|
||||
newHash := hashKey(newKey)
|
||||
|
||||
// 插入新 Key,继承旧 Key 的所有属性
|
||||
_, err = a.db.Exec(
|
||||
`INSERT INTO api_keys (app_id, tenant_id, name, key_hash, allowed_models, allowed_priorities, is_admin, enabled, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
||||
appID, tenantID, name+" (rotated)", newHash, allowedModelsJSON, allowedPrioritiesJSON, isAdmin, expiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("insert rotated key: %w", err)
|
||||
}
|
||||
|
||||
// 禁用旧 Key
|
||||
_, err = a.db.Exec(`UPDATE api_keys SET enabled = 0 WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("disable old key: %w", err)
|
||||
}
|
||||
|
||||
// 更新内存:移除旧 Key,添加新 Key
|
||||
identity := &AppIdentity{
|
||||
AppID: appID,
|
||||
TenantID: tenantID,
|
||||
Name: name + " (rotated)",
|
||||
IsAdmin: isAdmin == 1,
|
||||
}
|
||||
if allowedModelsJSON != "" && allowedModelsJSON != "null" {
|
||||
json.Unmarshal([]byte(allowedModelsJSON), &identity.AllowedModels)
|
||||
}
|
||||
if allowedPrioritiesJSON != "" && allowedPrioritiesJSON != "null" {
|
||||
json.Unmarshal([]byte(allowedPrioritiesJSON), &identity.AllowedPriorities)
|
||||
}
|
||||
if expiresAt.Valid && expiresAt.String != "" {
|
||||
if t, err := time.Parse(time.RFC3339, expiresAt.String); err == nil {
|
||||
identity.ExpiresAt = &t
|
||||
}
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
delete(a.keys, oldHash)
|
||||
a.keys[newHash] = identity
|
||||
a.mu.Unlock()
|
||||
|
||||
return newKey, nil
|
||||
}
|
||||
|
||||
// Middleware returns an HTTP middleware that enforces API Key authentication.
|
||||
func (a *Authenticator) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -219,12 +408,13 @@ func RequireAdmin(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// CheckModelPermission verifies the app can access the given model.
|
||||
// 支持通配符 "*" 匹配所有模型。
|
||||
func CheckModelPermission(identity *AppIdentity, model string) bool {
|
||||
if len(identity.AllowedModels) == 0 {
|
||||
return true // empty = all models allowed
|
||||
}
|
||||
for _, m := range identity.AllowedModels {
|
||||
if m == model {
|
||||
if m == model || m == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -251,10 +441,33 @@ func isAdminInt(b bool) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
// Close closes the database connection and stops background cleanup.
|
||||
func (a *Authenticator) Close() error {
|
||||
close(a.stopCh)
|
||||
return a.db.Close()
|
||||
}
|
||||
|
||||
// cleanupExpiredKeys 定期清理内存中已过期的 API Key,防止内存泄漏。
|
||||
func (a *Authenticator) cleanupExpiredKeys() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
a.mu.Lock()
|
||||
for hash, identity := range a.keys {
|
||||
if identity.ExpiresAt != nil && now.After(*identity.ExpiresAt) {
|
||||
delete(a.keys, hash)
|
||||
}
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case <-a.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure middleware import is used.
|
||||
var _ = middleware.GetRequestID
|
||||
|
||||
Reference in New Issue
Block a user