security_policy_session.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package service
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "encoding/json"
  6. "strconv"
  7. "strings"
  8. "github.com/gin-gonic/gin"
  9. "github.com/tidwall/gjson"
  10. )
  11. // maxSecurityPolicyTranscriptLookupKeys 与 cyber 一致:限定单次 Redis 查询量,
  12. // 保留最新的 transcript 前缀(续聊最可能命中)。
  13. const maxSecurityPolicyTranscriptLookupKeys = 256
  14. // SecurityPolicySessionScopeKey 会话封禁的 Redis HASH scope。
  15. // 按分组+API Key 隔离:换 Key 即新身份,管理员可按 scope 整体解封,无需扫描。
  16. func SecurityPolicySessionScopeKey(groupID, apiKeyID int64) string {
  17. return "secpol:scope:" + strconv.FormatInt(groupID, 10) + ":" + strconv.FormatInt(apiKeyID, 10)
  18. }
  19. func hashSecurityPolicySessionField(raw string) string {
  20. sum := sha256.Sum256([]byte(raw))
  21. return "k:" + hex.EncodeToString(sum[:16])
  22. }
  23. // SecurityPolicyExplicitSessionKey 显式会话标识:通用头
  24. // (session_id/x-session-id/conversation_id)优先;OpenAI 系协议额外复用
  25. // 既有 cyber 显式 key(含 responses 转录回退语义)。
  26. func SecurityPolicyExplicitSessionKey(protocol string, apiKeyID int64, c *gin.Context, body []byte) string {
  27. if c != nil && c.Request != nil {
  28. for _, header := range []string{"session_id", "x-session-id", "conversation_id"} {
  29. if id := strings.TrimSpace(c.Request.Header.Get(header)); id != "" {
  30. return hashSecurityPolicySessionField("secpol-explicit:v1|api_key=" + strconv.FormatInt(apiKeyID, 10) + "|" + id)
  31. }
  32. }
  33. }
  34. switch protocol {
  35. case ContentModerationProtocolOpenAIResponses,
  36. ContentModerationProtocolOpenAIChat:
  37. if key := CyberSessionExplicitBlockKey(apiKeyID, c, body); key != "" {
  38. return hashSecurityPolicySessionField(key)
  39. }
  40. }
  41. return ""
  42. }
  43. // SecurityPolicyTranscriptKeys 会话转录 lineage key:同一对话的后续轮次必然
  44. // 包含已封禁前缀,从而被拦截;新会话(新 conversation/新 transcript)自然放行。
  45. // 为避免“相同首句”误伤不同会话,仅当转录含模型已生成内容(即续聊)时才出 key。
  46. func SecurityPolicyTranscriptKeys(protocol string, apiKeyID int64, body []byte) []string {
  47. switch protocol {
  48. case ContentModerationProtocolOpenAIResponses,
  49. ContentModerationProtocolOpenAIChat:
  50. return CyberSessionTranscriptBlockKeys(apiKeyID, body)
  51. default:
  52. return securityPolicyGenericTranscriptKeys(protocol, apiKeyID, body)
  53. }
  54. }
  55. // SecurityPolicySessionLookupKeys 本次请求全部待查 key:显式优先,其次转录。
  56. func SecurityPolicySessionLookupKeys(protocol string, apiKeyID int64, c *gin.Context, body []byte) []string {
  57. keys := make([]string, 0, 8)
  58. if explicit := SecurityPolicyExplicitSessionKey(protocol, apiKeyID, c, body); explicit != "" {
  59. keys = append(keys, explicit)
  60. }
  61. transcript := SecurityPolicyTranscriptKeys(protocol, apiKeyID, body)
  62. seen := make(map[string]struct{}, len(transcript)+1)
  63. for _, key := range keys {
  64. seen[key] = struct{}{}
  65. }
  66. for _, key := range transcript {
  67. if key == "" {
  68. continue
  69. }
  70. if _, dup := seen[key]; dup {
  71. continue
  72. }
  73. seen[key] = struct{}{}
  74. keys = append(keys, key)
  75. if len(keys) >= maxSecurityPolicyTranscriptLookupKeys+1 {
  76. break
  77. }
  78. }
  79. return keys
  80. }
  81. func securityPolicyGenericTranscriptKeys(protocol string, apiKeyID int64, body []byte) []string {
  82. if len(body) == 0 {
  83. return nil
  84. }
  85. var root gjson.Result
  86. switch protocol {
  87. case ContentModerationProtocolAnthropicMessages:
  88. root = gjson.GetBytes(body, "messages")
  89. case ContentModerationProtocolGemini:
  90. root = gjson.GetBytes(body, "contents")
  91. default:
  92. return nil
  93. }
  94. if !root.IsArray() {
  95. return nil
  96. }
  97. h := sha256.New()
  98. _, _ = h.Write([]byte("secpol-transcript:v1|api_key="))
  99. _, _ = h.Write([]byte(strconv.FormatInt(apiKeyID, 10)))
  100. keys := make([]string, 0, 8)
  101. nextSlot := 0
  102. rotated := false
  103. lastKey := ""
  104. hasModelGenerated := false
  105. root.ForEach(func(_, item gjson.Result) bool {
  106. role, text := securityPolicyTranscriptItemText(protocol, item)
  107. if strings.TrimSpace(text) == "" {
  108. return true
  109. }
  110. if securityPolicyTranscriptItemIsModelGenerated(protocol, role) {
  111. hasModelGenerated = true
  112. }
  113. canonical, _ := json.Marshal(map[string]string{"role": role, "text": text})
  114. _, _ = h.Write([]byte("|item="))
  115. _, _ = h.Write(canonical)
  116. lastKey = hex.EncodeToString(h.Sum(nil))
  117. if len(keys) < maxSecurityPolicyTranscriptLookupKeys {
  118. keys = append(keys, lastKey)
  119. } else {
  120. keys[nextSlot] = lastKey
  121. nextSlot = (nextSlot + 1) % maxSecurityPolicyTranscriptLookupKeys
  122. rotated = true
  123. }
  124. return true
  125. })
  126. if !hasModelGenerated {
  127. // 首轮(无模型历史):不输出 lineage key,避免相同首句误伤其他会话。
  128. return nil
  129. }
  130. if rotated {
  131. ordered := make([]string, 0, len(keys))
  132. ordered = append(ordered, keys[nextSlot:]...)
  133. ordered = append(ordered, keys[:nextSlot]...)
  134. return ordered
  135. }
  136. return keys
  137. }
  138. // securityPolicyTranscriptItemText 提取单条消息的角色与纯文本。
  139. func securityPolicyTranscriptItemText(protocol string, item gjson.Result) (string, string) {
  140. switch protocol {
  141. case ContentModerationProtocolAnthropicMessages:
  142. role := strings.ToLower(strings.TrimSpace(item.Get("role").String()))
  143. content := item.Get("content")
  144. if content.Type == gjson.String {
  145. return role, content.String()
  146. }
  147. var sb strings.Builder
  148. content.ForEach(func(_, block gjson.Result) bool {
  149. if text := block.Get("text"); text.Type == gjson.String && strings.TrimSpace(text.String()) != "" {
  150. if sb.Len() > 0 {
  151. sb.WriteString("\n")
  152. }
  153. sb.WriteString(text.String())
  154. }
  155. return true
  156. })
  157. return role, sb.String()
  158. default: // gemini contents[].parts[].text
  159. role := strings.ToLower(strings.TrimSpace(item.Get("role").String()))
  160. var sb strings.Builder
  161. item.Get("parts").ForEach(func(_, part gjson.Result) bool {
  162. if text := part.Get("text"); text.Type == gjson.String && strings.TrimSpace(text.String()) != "" {
  163. if sb.Len() > 0 {
  164. sb.WriteString("\n")
  165. }
  166. sb.WriteString(text.String())
  167. }
  168. return true
  169. })
  170. return role, sb.String()
  171. }
  172. }
  173. func securityPolicyTranscriptItemIsModelGenerated(protocol, role string) bool {
  174. switch role {
  175. case "assistant", "model":
  176. return true
  177. }
  178. return false
  179. }
  180. // SecurityPolicySessionStoreKeyForAdmin 供管理端按分组+Key 解封时复用同一 scope。
  181. func SecurityPolicySessionStoreKeyForAdmin(groupID, apiKeyID int64) string {
  182. return SecurityPolicySessionScopeKey(groupID, apiKeyID)
  183. }