400 万个账号、缓刑 3 年:大模型平台的接口鉴权漏在哪
2026 年 11 月 2 日,上海法治报报道了上海市首例利用 AI 伪造签名、盗用大模型算力案,案件已于近日宣判。被告人宗某某在 2025 年 12 月自学了调用 AI 平台 API 接口的“教程”,随后盯上一家人工智能企业运营的大模型聚合平台。这个平台的计费逻辑不复杂:用户登录后消耗积分生成图片和视频,平台按次向上游模型厂商支付算力成本。宗某某用技术手段摸到了平台不对外开放的接口,自建中转网站,对外低价售卖 AI 绘图和视频生成服务,后台跑的却是聚合平台的付费算力。报道标题给出的数字是 400 多万个账号,判决是 3 年有期徒刑、缓刑 3 年。
这条新闻里最有技术含量的一句,是“不对外开放的接口”。它通常意味着几种情况:内部服务之间的调用端点、前端隐藏但未做服务端校验的路由、测试环境遗留的入口,或者只靠一个不公开的 URL 撑着的“私有 API”。共同点是鉴权依赖路径未知,而非凭证有效。一旦有人抓到流量、翻到打包进前端的接口名,或者撞库拿到一个低权限账号,后面就只剩成本问题。
聚合平台的账,漏在哪一层
聚合平台的成本结构和普通 SaaS 不一样。普通 SaaS 的边际成本主要是带宽和计算,扩容能摊薄;聚合平台每生成一张图、一段视频,都要按次向上游厂商付钱。用户侧的积分是预付费或补贴,上游的账单是实时的。积分系统和上游计费之间是一个杠杆:只要有人能用极低成本拿到积分,或者干脆绕过积分直接打到模型接口上,杠杆就反向作用,边际成本全部落在平台自己身上。
报道里提到宗某某先摸了接口,再建中转站。这个顺序说明他找到的不是“注册小号领积分”这种运营层的漏洞,而是更靠下的接口层。积分是可以风控的,接口不行。一个没有绑定身份、没有时效、没有调用方签名约束的端点,只要被扫到,就等于对外开放。
标题里那句“AI 伪造签名”值得单独看。放在 API 语境下,签名一般指请求签名、设备指纹或回调校验。伪造签名能成立,通常意味着签名算法或其密钥暴露在客户端,或者服务端只校验“有没有签名”而不校验“签名是否由可信方在有效时间内对这段内容签发”。前一种是把密钥放进了不该放的地方,后一种是把校验做成了形式。
鉴权要补的三件事
第一件是让每个请求都绑定到一个可追溯的调用方。短期 token 比长期 API key 好,token 里带租户 ID、模型白名单和过期时间,服务端校验签名而不是校验字符串相等。第二件是防重放,时间戳加 nonce,nonce 落 Redis 并设置不小于时间窗口的 TTL。第三件是成本侧的熔断,按模型单价加权统计每个租户的消耗,而不是只数请求数。一张图和一个视频的单价差一个数量级,只按 QPS 限流等于没限。
下面两段代码是演示用法,键名、窗口和密钥管理方式按自己的环境替换。
请求签名校验,用 HMAC 对“时间戳 + nonce + 请求体”签名,服务端做常量时间比较:
import hmac, hashlib, time
from fastapi import Request, HTTPException
WINDOW = 300
SECRET = b"load-from-kms" # 演示写法,生产环境从 KMS 或环境变量取
def verify(req: Request, body: bytes):
ts = req.headers.get("x-ts")
nonce = req.headers.get("x-nonce")
sig = req.headers.get("x-sign")
if not (ts and nonce and sig):
raise HTTPException(401, "missing signature headers")
if abs(time.time() - int(ts)) > WINDOW:
raise HTTPException(401, "timestamp out of window")
expected = hmac.new(
SECRET, f"{ts}.{nonce}.".encode() + body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
raise HTTPException(401, "bad signature")
# 演示:nonce 需写入 Redis,SET key 1 NX EX WINDOW,失败即判定重放
固定窗口限流用 Lua 保证 INCR 和 EXPIRE 的原子性,避免第一次请求拿到 key 但没设过期时间:
import redis
r = redis.Redis()
SCRIPT = """
local c = redis.call('INCR', KEYS[1])
if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return c
"""
def allow(key: str, limit: int, window: int) -> bool:
current = r.eval(SCRIPT, 1, key, window)
return int(current) <= limit
限流键建议拼成 rl:{tenant_id}:{model}:{minute_bucket},租户维度优先于 IP 维度。固定窗口在边界上会有两倍突发,量级敏感的场景换成令牌桶或滑动窗口。
踩坑点
nonce 的 TTL 必须大于等于时间戳窗口,否则窗口内被清理掉的 nonce 可以重放;反过来窗口设得太宽,比如十分钟,业务上又基本没有重放空间,攻击者只需要在这十分钟里复用一次就够了。签名校验一定要用 hmac.compare_digest 这类常量时间比较,普通字符串相等在时间侧信道上会泄漏前缀。
限流键只用 IP 会误伤。大量用户走 NAT 出口,一个 IP 背后可能是整栋写字楼;反过来攻击者换 IP 的成本很低。账号、设备、租户三个维度叠起来,权重可以不同,但别只留一个。
成本熔断要按模型单价加权。资讯里提到的场景是图片和视频生成,这两类调用的上游单价差距很大,如果配额按“次数”平均分配,攻击者会集中打最贵的那个模型。更稳的做法是给每个租户一个“金额配额”,每次调用按模型单价预扣,调用结束后按实际用量结算,差额退回。
内部接口要过网关。很多团队的服务间调用是直连的,网关上的鉴权和限流对它们不生效。一旦某个内部端点因为调试或历史原因暴露到公网,就等于绕过了所有防线。服务网格或 mTLS 能解决身份问题,但解决不了“这个端点该不该被外部访问”的问题,后者得靠网络策略。
这套方案能挡住什么,挡不住什么
客户端签名能提高门槛,挡不住有心人。密钥只要下发到客户端,泄漏就是时间问题,混淆只能拖延。真正的主防线是服务端签发的短期凭证加上调用方的身份绑定,客户端那层签名是筛子,不是墙。
限流和配额能控制损失,控制不了意图。一个合法租户把配额用满,和攻击者盗用配额,在计数器上看起来一样。区别要靠行为特征:调用时间分布、模型偏好、提示词相似度、出图结果的重复率。这部分没有通用配置,得按业务自己攒。
成本熔断是最后一道,也是唯一能兜住钱的。它不判断请求是否合法,只判断这个租户今天已经烧掉了多少预算,超过就降级到便宜模型或者直接拒绝。对一个按次向上游付费的聚合平台来说,这道闸门比任何鉴权都更接近问题的本质:你可以被绕过,但不能被绕到破产。
宗某某这个案子判了缓刑,400 多万个账号的规模摆在那里。真正值得追问的是,那家平台在案发之前,有没有一个地方能实时看到“某个租户今天调用了多少次最贵的视频模型”。如果没有,接口鉴权补得再漂亮,账还是后知后觉的。
更多推荐

所有评论(0)