LLM 网关提示词哈希鉴权与协议角色适配 (LLM Gateway Prompt Hash Guard and Role Adaptation)
1. 核心痛点与背景 (Core Challenges and Context)
在轻量级纯前端 Web Agent(如 InPage Agent)或无服务端会话的客户端应用中,通常会部署一个轻量级代理网关(Gateway)用于:
- 隐藏上游 API Key:防止用户的 API 密钥直接暴露在客户端浏览器的 Network 检查器中。
- 跨域与流式中继 (CORS & SSE Relay):处理浏览器同源策略,并为前端提供 SSE(Server-Sent Events)逐 Token 增量推送。
然而,单纯的 API Key 隐藏网关存在严重的安全漏洞:任何获取到网关接口地址的人,都可以直接将该网关当作免费的大模型中继(Proxy),发送任意提示词套用配额。
为了防范未授权代理滥用,同时确保客户端对动态上下文(如本地虚拟文件目录、技能注册表)的灵活注入,设计了提示词动静解耦、SHA-256 哈希签名鉴权与协议角色适配架构。
2. 核心架构设计 (Architecture Design)
flowchart TD
subgraph Client ["前端浏览器客户端 (Frontend Agent)"]
CSP["静态系统指令<br/>CORE_SYSTEM_PROMPT"]
DYN["动态工作区上下文<br/>(文件树/技能表)"]
REQ["请求体组装<br/>role: 'system' -> CORE_SYSTEM_PROMPT<br/>role: 'developer' -> 动态上下文"]
end
subgraph Gateway ["Go 代理网关 (Gateway Guard)"]
HG["1. 提示词哈希校验<br/>sha256(normalize(system)) == ALLOWED_HASH ?"]
FORBIDDEN["403 Forbidden<br/>拦截非法调用/篡改"]
ADAPT["2. 协议角色降级适配<br/>将 role: 'developer' 合并至 system<br/>剥离 developer 消息"]
UPSTREAM_REQ["组装上游兼容 Payload"]
end
subgraph Upstream ["模型供应商 (Upstream LLM Provider: DeepSeek)"]
MODEL["安全推理 & SSE 流式输出"]
end
CSP --> REQ
DYN --> REQ
REQ -->|"POST /v1/chat/completions"| HG
HG -- "哈希不匹配 / 无系统提示" --> FORBIDDEN
HG -- "哈希匹配通过" --> ADAPT
ADAPT --> UPSTREAM_REQ
UPSTREAM_REQ -->|"Forward with API Key"| MODEL
MODEL -->|"SSE Stream Response"| Gateway
Gateway -->|"SSE Flush"| Client2.1 提示词动静解耦 (Prompt Decoupling)
- 静态核心提示词 (
CORE_SYSTEM_PROMPT):固定定义在前端代码中,严格规定 Agent 的职责、工具列表与调用规范。该内容在运行时不随用户项目变化。 - 动态上下文提示词 (
developerPrompt):包含当前虚拟文件系统(VFS)的文件列表结构、当前激活的技能插件等。动态内容通过 OpenAI 规范中的developer角色消息传递,保持与静态system消息隔离。
2.2 跨平台字符串归一化与 SHA-256 校验 (Normalization & Hash Verification)
不同操作系统开发环境下的行尾换行符可能存在差异(Windows 的 \r\n 与 Linux/macOS 的 \n)。网关在计算哈希前必须进行归一化处理:
normalizedSystem := strings.TrimSpace(strings.ReplaceAll(systemPrompt, "\r\n", "\n"))
sum := sha256.Sum256([]byte(normalizedSystem))
computedHash := hex.EncodeToString(sum[:])如果传入的 system 提示词哈希与网关环境变量 GATEWAY_SYSTEM_PROMPT_HASH 允许的哈希不符,立即返回 HTTP 403 Forbidden: {"error":{"message":"forbidden: invalid system prompt hash"}},彻底杜绝恶意篡改与未授权调用。
2.3 异构上游的角色兼容性适配 (Upstream Role Adaptation)
虽然 OpenAI 官方在 o1/GPT-4o 系列中支持 developer 角色,但许多主流开源与商业模型接口(例如 DeepSeek API)对请求体中的角色枚举进行了严格校验,接收到 role: "developer" 会直接报错抛出 HTTP 400 Bad Request: unknown variant 'developer', expected one of 'system', 'user', 'assistant', 'tool'。
网关在校验通过后执行降级合并:
- 提取所有
role == "developer"消息的文本内容。 - 将其拼接追加至已通过校验的
role == "system"消息末尾。 - 从
messages数组中移除原developer项。 - 将净化并兼容后的 Payload 序列化并转发给上游。
3. Go 网关核心实现 (Go Gateway Implementation)
func (c Config) handleChat(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, c.MaxBytes))
if err != nil {
http.Error(w, `{"error":{"message":"request body too large"}}`, http.StatusRequestEntityTooLarge)
return
}
var req map[string]any
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, `{"error":{"message":"bad json body"}}`, http.StatusBadRequest)
return
}
// 1. 核心校验:核对系统提示词 SHA-256 哈希
if len(c.AllowedPromptHashes) > 0 {
var systemPrompt string
if rawMsgs, ok := req["messages"].([]any); ok {
for _, item := range rawMsgs {
if msgMap, ok := item.(map[string]any); ok {
if role, _ := msgMap["role"].(string); role == "system" {
systemPrompt = extractTextContent(msgMap["content"])
break
}
}
}
}
normalizedSystem := strings.TrimSpace(strings.ReplaceAll(systemPrompt, "\r\n", "\n"))
sum := sha256.Sum256([]byte(normalizedSystem))
computedHash := hex.EncodeToString(sum[:])
matched := false
for _, allowed := range c.AllowedPromptHashes {
if strings.EqualFold(allowed, computedHash) {
matched = true
break
}
}
if !matched {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error":{"message":"forbidden: invalid system prompt hash"}}`))
return
}
}
// 2. 动态 Developer 消息适配上游:合并至 system 角色,消除 DeepSeek 等上游 400 报错
if rawMsgs, ok := req["messages"].([]any); ok {
var newMsgs []any
var developerContents []string
var sysMsgMap map[string]any
for _, item := range rawMsgs {
msgMap, ok := item.(map[string]any)
if !ok {
newMsgs = append(newMsgs, item)
continue
}
role, _ := msgMap["role"].(string)
if role == "developer" {
if text := extractTextContent(msgMap["content"]); text != "" {
developerContents = append(developerContents, text)
}
continue // 剥离 developer 消息
}
if role <mark> "system" && sysMsgMap </mark> nil {
sysMsgMap = msgMap
}
newMsgs = append(newMsgs, msgMap)
}
if len(developerContents) > 0 {
joinedDev := strings.Join(developerContents, "\n\n")
if sysMsgMap != nil {
curr := extractTextContent(sysMsgMap["content"])
sysMsgMap["content"] = curr + "\n\n" + joinedDev
}
}
req["messages"] = newMsgs
}
// 3. 构造上游请求并启动流式透传
// ...
}4. 关键踩坑与防范规范 (Lessons & Pitfalls)
- 校验顺序绝不能颠倒:必须先对纯净的
CORE_SYSTEM_PROMPT做哈希匹配,通过之后再合并developer内容。若先合并再计算哈希,动态文件列表和技能变更会导致每次计算出的哈希都不相同,从而破坏签名校验机制。 - 单双引号与环境变量解析:在 Go 实现简易
.env加载时,必须去除值两端的包裹单/双引号,避免解析出带有字面量引号的哈希值或密钥。 - 多哈希兼容与灰度发布:生产网关可能需要同时支持正在运行的旧版本前端与新上线的前端,
GATEWAY_SYSTEM_PROMPT_HASH采用英文逗号分隔支持多个有效 Hash,支持平滑热升级与灰度发布。 - SSE 流式传输保活:网关代理上游响应时需检查
http.Flusher,在读取到上游块后立即flusher.Flush(),杜绝缓冲区阻塞导致的客户端流式卡顿。
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps