签名验证必须在视图外层用中间件统一拦截,校验method+path+timestamp+nonce+body+app_key,缓存request.body防重复读,app_secret加密存库并Redis原子校验nonce。

签名验证必须在视图最外层做,不能放在业务逻辑里
签名失效、时间戳过期、重放请求这些校验,如果混在 APIView 的 post() 方法里,很容易被绕过——比如开发者后续加了缓存装饰器、权限中间件顺序错乱,或者用了 @action 路由但忘了复刻校验逻辑。正确做法是用 Django 中间件或自定义装饰器统一拦截所有 API 请求,在进入路由分发前就完成基础签名验证。
推荐用中间件,因为能覆盖所有 /api/ 路径,且不依赖视图类写法。关键点:只对指定前缀(如 /api/v1/)和特定 HTTP 方法(POST、PUT、PATCH)做校验;跳过登录、健康检查等免签接口;校验失败直接返回 401 或 403,不进视图。
- 时间戳误差容忍建议设为
300秒(5 分钟),太短影响弱网用户,太长增加重放窗口 - 签名算法必须包含
method+path+timestamp+nonce+body(若存在)+app_key,缺一不可 - 不要用
request.GET拼接签名原文——URL 编码不一致会导致签名不匹配,应统一用原始请求路径 + 解析后的 query dict 排序后拼接
body 签名必须读取一次且仅一次
Django 的 request.body 是个流对象,读完就空了。如果在签名验证时调用了一次 request.body,后面视图里再读就会得到空字节串,导致 JSONParser 解析失败或 form-data 丢失字段。
解决办法是在中间件里把原始 body 缓存到 request._cached_body,并重置 request._body,让后续解析正常工作。同时注意:如果请求是 multipart/form-data,不能直接读 body,得从 request.POST 和 request.FILES 提取参数再构造签名原文(通常这类接口不建议签名 body,改用 query + header 更稳妥)。
提供使用 django-unfold (v0.56+) 构建高级 Django 管理界面的专业指导。当用户提及 django-unfold、unfold admin 等内容时使用此技能。
- 对 JSON 接口:用
json.loads(request.body.decode())构造有序键的字符串参与签名,但必须先request.body.seek(0)或缓存原始 bytes - 避免在签名逻辑里调用
request.data或request.json()——它们会触发 DRF 的解析器,破坏原始流 - 测试时用
curl -H "Content-Type: application/json" -d '{"a":1}' ...,别用表单格式混用
密钥管理不能硬编码,也不该存在 settings.py 里
SECRET_KEY 是全局加密用的,不能拿来当接口签名密钥——它一旦泄露,整个站点加密体系崩塌。每个接入方(App)应该有独立的 app_secret,且不能明文写在代码或配置文件中。
推荐方案:把 app_key → app_secret 映射存在数据库(如 AppCredential 模型),查库时加缓存(cache.get_or_set),并设置合理过期时间(如 5 分钟)。查询失败或密钥不匹配,一律按“无效签名”处理,不暴露是否存在该 app_key。
- 数据库字段
app_secret必须用django.contrib.auth.hashers.make_password()加密存储,不能明文或 base64 - 不要用环境变量传
app_secret——运维部署时容易泄漏,且无法按 app 动态轮换 - 签名计算时用
hmac.new(key.encode(), msg.encode(), digestmod='sha256').hexdigest(),别用hashlib.md5这种已被证明不安全的算法
重放攻击靠 nonce + redis 实现,别依赖数据库唯一索引
只靠数据库 unique_together = ('app_key', 'nonce') 挡不住高并发重放:两个相同请求几乎同时到达,可能都通过唯一性校验(数据库事务隔离级别不足)。真正可靠的做法是用 Redis 做原子性 setnx + expire。
流程:收到请求后,用 f"{app_key}:{nonce}" 为 key,执行 redis.setex(key, 300, "1");若返回 False,说明已存在,拒绝请求。key 过期时间必须等于时间戳容忍窗口(如 300 秒),否则旧 nonce 可能被重复利用。
- Redis 连接必须配置连接池,避免每次校验都新建连接拖慢响应
- 如果 Redis 不可用,应降级为只校验时间戳(不校验 nonce),而不是直接放过——宁可误杀,不可放行
- nonce 长度建议 ≥ 16 字符,用
secrets.token_urlsafe(16)生成,别用时间戳或自增 ID

















