Files
opencatd-open/backend/middleware/auth_llm.go
T
Sakurasan f9e9a1572f feat: 系统设置新增原始请求/响应记录开关(仅管理员)
- 系统配置键 log_raw_requests:开启后,仅管理员账号的每次请求
  在用量明细中保存客户端原始请求体与上游原始响应体
  (流式含全部 SSE 事件),用于排障
- UsageLog 新增 raw_request / raw_response 字段(type:text)
- AuthLLM 附带 user_role 供网关判断管理员
- gateway:10s TTL 缓存开关;streamResponse/bufferResponse
  支持累积上游原始响应;recordUsage 填充原始字段
- 前端 SystemConfig 新增开关(会显著增加存储的提示)
- 新增 doc/flow.md 网关调用流程示意图
2026-09-01 02:32:31 +08:00

74 lines
2.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"net/http"
"opencatd-open/internal/store"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// keyPrefixLen 是 key_prefix 列的截断长度,必须与 api.go:459 的 keyValue[:12] 一致。
// 真实 key 为 sk-ot- + 48 位 hex(54 字符),故 12 位足够唯一。
const keyPrefixLen = 12
func AuthLLM(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
key := extractAPIKey(c.GetHeader("Authorization"))
// 区分「没传」和「传了但不对」,便于排查客户端配置。
if strings.TrimSpace(c.GetHeader("Authorization")) == "" {
unauthorized(c, "未提供认证信息")
return
}
// 长度不足时直接拒绝:避免下方 authToken[:12] 越界 panic 打崩进程。
if len(key) < keyPrefixLen {
unauthorized(c, "无效的API密钥")
return
}
var apiKey store.APIKey
if err := db.Where("key_prefix = ? AND status = ?", key[:keyPrefixLen], store.KeyStatusActive).First(&apiKey).Error; err != nil {
unauthorized(c, "无效的API密钥")
return
}
// Verify full key hash
if apiKey.KeyHash != store.HashAPIKey(key) {
unauthorized(c, "无效的API密钥")
return
}
// 附带用户角色(判断是否管理员,供原始请求/响应记录等管理能力使用)。
var user store.User
if err := db.First(&user, apiKey.UserID).Error; err != nil {
unauthorized(c, "无效的API密钥")
return
}
c.Set("api_key", &apiKey)
c.Set("user_id", apiKey.UserID)
c.Set("user_role", user.Role)
c.Next()
}
}
// extractAPIKey 从 Authorization 头取 Bearer token,兼容无 "Bearer " 前缀的直传。
func extractAPIKey(auth string) string {
auth = strings.TrimSpace(auth)
if strings.HasPrefix(auth, "Bearer ") {
return strings.TrimSpace(auth[len("Bearer "):])
}
return auth
}
func unauthorized(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": map[string]interface{}{
"message": message,
"type": "invalid_request_error",
},
})
}