单纯校验时间戳会失效,因攻击者可截获并重放合法请求;必须将时间戳与请求体联合签名(如hmac-sha256),确保请求唯一性,且签名原文需规范拼接、密钥安全存储、客户端服务端序列化一致。

为什么单纯校验时间戳会失效
服务端收到请求后只比对 timestamp 是否在 5 分钟内,看似合理,但攻击者可截获合法请求、稍作修改(比如改个订单金额)再原样重放——只要时间戳仍在窗口期内,校验就通过。本质是缺了「请求唯一性绑定」,时间戳只是防重放的必要条件,不是充分条件。
真正要防的是「同一请求体被重复提交」,不是「旧请求被延迟提交」。所以必须把时间戳和请求内容(或其摘要)联合签名,且签名不可被篡改或复用。
用 HMAC-SHA256 绑定时间戳与请求体
推荐在请求头中携带 X-Signature 和 X-Timestamp,服务端用共享密钥对 timestamp + body 做 HMAC 签名比对。注意:body 必须是原始字节、未格式化、无换行/空格干扰;若用 JSON,需确保客户端和服务端序列化方式一致(例如都禁用缩进、字段顺序固定)。
-
timestamp必须是 Unix 时间戳秒级整数(不是毫秒),避免时区/精度差异 - 签名原文拼接格式建议为:
timestamp + "\n" + string(body),用换行符分隔防止粘连(如1717023456{"a":1}可能和171702345{"6":{"a":1}}冲突) - 密钥绝不能硬编码在代码里,应从环境变量或 secret manager 加载,例如
os.Getenv("API_SECRET_KEY")
Gin 框架中的中间件实现要点
在 Gin 的 gin.HandlerFunc 中做校验,关键是要提前读取并缓存 c.Request.Body,因为默认 Body 是单次读取流,校验签名后还要交给后续 handler 解析,否则会变空。
- 用
io.ReadAll(c.Request.Body)读取原始 body 字节,然后用c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))重置 - 时间戳校验必须在签名前做:先检查
X-Timestamp是否为有效整数、是否在当前时间 ±300 秒内(防止客户端时间偏差) - 签名失败统一返回
401 Unauthorized,不要暴露是时间戳错还是签名错,避免信息泄露 - 若接口允许 GET 请求带参数防重放,需将 query string 归一化后参与签名(如按 key 字典序拼接
key1=val1&key2=val2)
客户端容易漏掉的三个细节
很多前端或移动端同学调通一次就以为万事大吉,结果压测时大量 401——问题往往出在客户端构造签名的环节。
- HTTP 方法名必须大写且严格匹配(
"POST"≠"post"),Gin 默认路由不区分大小写,但签名逻辑必须区分 - Content-Type 头若存在(如
application/json),应在签名原文中显式包含,否则服务端解析时可能自动补 default 值导致不一致 - 时间戳生成必须用系统实时时间,不能用本地 JS
Date.now()毫秒值除以 1000 后取整——浮点误差可能导致秒级偏差;应调用服务端 `/time` 接口同步时间,或用 NTP 客户端校准
最麻烦的其实是 body 序列化一致性:Go 服务端用 json.Marshal,前端用 JSON.stringify,但后者默认不排序字段、会忽略 undefined,而前者会转成 null。这类细微差异会让签名永远对不上。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











