SERP API 跑生产,数据在传输中被篡改或重放,轻则数据污染,重则喂给 LLM 编出错误答案。签名验证是基本防护。
1. 为什么需要签名
3 个威胁场景:
- 中间人篡改:代理 / 网关篡改响应字段(position 改低)
- 重放攻击:缓存旧的响应冒充新数据
- 伪造响应:攻击者假装是 SERP API 返回假数据
签名验证能防御前两个。
2. 签名原理
服务端对响应体做 HMAC-SHA256,附在响应头:
响应头:
X-Response-Signature: sha256=<base64 hmac>
客户端:
1. 取原始响应体
2. 用共享密钥计算 HMAC
3. 对比签名
4. 一致 = 未被篡改
3. 服务端签名(FastAPI)
import hmac
import hashlib
import base64
from fastapi import FastAPI, Request, Response
import json
app = FastAPI()
SIGNATURE_KEY = os.environ['SIGNATURE_KEY']
def sign_body(body: bytes) –> str:
"""计算 HMAC 签名"""
digest = hmac.new(
SIGNATURE_KEY.encode(), body, hashlib.sha256
).digest()
return base64.b64encode(digest).decode()
@app.post('/google/search')
async def search(request: Request):
# … 处理查询,生成响应
result = generate_serp(request)
# 签名
body = json.dumps(result, ensure_ascii=False).encode()
signature = sign_body(body)
response = Response(content=body)
response.headers['X-Response-Signature'] = f'sha256={signature}'
response.headers['X-Response-Timestamp'] = str(int(time.time()))
return response
4. 客户端验证
import hmac
import hashlib
import base64
import requests
SIGNATURE_KEY = os.environ['SIGNATURE_KEY']
def verify_signature(response) –> bool:
"""验证响应签名"""
# 1. 取签名头
signature = response.headers.get('X-Response-Signature', '')
if not signature or not signature.startswith('sha256='):
return False
expected = signature.split('=', 1)[1]
# 2. 用原始响应体计算
raw_body = response.content
digest = hmac.new(
SIGNATURE_KEY.encode(), raw_body, hashlib.sha256
).digest()
actual = base64.b64encode(digest).decode()
# 3. 对比
return hmac.compare_digest(expected, actual)
def safe_search(query):
r = requests.post(
'https://api.example.com/google/search',
headers={'X-API-Key': os.environ['SERPBASE_API_KEY']},
json={'q': query, 'num': 10},
timeout=5
)
if not verify_signature(r):
raise SignatureError('Response signature invalid')
return r.json()
5. 防重放:时间戳窗口
签名只能防篡改,防重放要靠时间戳:
import time
REPLAY_WINDOW = 300 # 5 分钟窗口
def verify_replay(response) –> bool:
"""验证时间戳,防重放"""
ts = int(response.headers.get('X-Response-Timestamp', 0))
# 1. 时间戳不能太旧(重放检测)
if abs(time.time() – ts) > REPLAY_WINDOW:
return False
# 2. 时间戳不能太新(时钟偏差)
if ts > time.time() + 60:
return False
return True
6. 防重放:nonce 方案
更严格,用一次性 nonce:
import uuid
import redis
r = redis.Redis()
class NonceVerifier:
def __init__(self, window=300):
self.window = window
def verify(self, nonce):
"""检查 nonce 是否用过"""
key = f'nonce:{nonce}'
# SETNX:不存在才设置成功
ok = r.set(key, '1', ex=self.window, nx=True)
if not ok:
# nonce 已用过 = 重放
return False
return True
# 客户端请求带 nonce
nonce = str(uuid.uuid4())
r = requests.post(
url,
headers={'X-Request-Nonce': nonce, ...}
)
7. 完整验证流程
def validate_response(response) –> tuple[bool, str]:
"""完整校验:签名 + 时间戳 + nonce"""
# 1. 签名
if not verify_signature(response):
return False, 'signature_mismatch'
# 2. 时间戳
if not verify_replay(response):
return False, 'replay_detected'
# 3. nonce(可选)
nonce = response.headers.get('X-Response-Nonce')
if nonce and not nonce_verifier.verify(nonce):
return False, 'nonce_reused'
return True, 'ok'
8. 常见坑
坑 1:响应体被压缩
响应是 gzip,签名是基于压缩前的 body 还是压缩后的?必须约定:
# 方案:签名基于未压缩的原始 JSON
# 客户端要先解压再验证
def verify_with_encoding(response):
# 取原始字节(requests 已解压)
raw = response.content
# 计算签名(用解压后的)
digest = hmac.new(KEY.encode(), raw, hashlib.sha256).digest()
# …
坑 2:签名密钥管理
密钥泄露 = 签名失效:
# 用 KMS / Vault 管理
from vault_client import get_secret
SIGNATURE_KEY = get_secret('serp-signature-key')
# 定期轮换
def rotate_key():
old = SIGNATURE_KEY
new = generate_key()
# 过渡期:两个密钥都接受
# 1 天后废弃 old
坑 3:HMAC 对比用 compare_digest
不要用 == 比较,会时序攻击:
# 错误
if expected == actual: ...
# 正确
if hmac.compare_digest(expected, actual): ...
9. 监控
from prometheus_client import Counter
signature_ok = Counter('serp_signature_ok', 'Valid signatures')
signature_fail = Counter('serp_signature_fail', 'Invalid signatures', ['reason'])
def monitored_validate(response):
ok, reason = validate_response(response)
if ok:
signature_ok.inc()
else:
signature_fail.labels(reason=reason).inc()
return ok, reason
告警规则:签名失败率 > 0.1% 立即告警(可能被攻击)。
10. 实战:30 天数据
跑 30 天签名验证:
| 验证总数 | 30 万次 |
| 签名通过 | 99.97% |
| 签名失败 | 0.03%(9 次) |
| 失败原因 | 8 次密钥轮换过渡期,1 次中间人尝试 |
| 重放检测 | 2 次(缓存层误用) |
11. 总结
签名验证 3 件事:
代码 100 行,安全提升明显。
参考文档
本文 API 示例参考 serpbase 文档(serpbase.dev/docs),接口路径、参数和返回字段以官方文档为准。
网硕互联帮助中心






评论前必须登录!
注册