当前位置:首页 > 时尚

webapI安全验证header接口开发实例

去年帮一个做 SaaS 的安全朋友排查接口漏洞,发现他们所有 API 请求只靠一个明文 uid 做身份识别。验证抓包改个数字就能拿到别人公司的接口客户列表。这种设计放在内网都嫌糙,实例更别说挂在公网上。安全问题出在把"认证"和"授权"混为一谈,验证更准确地说,接口是实例根本没做认证。

HTTP 是安全无状态协议,每个请求都是验证独立的。服务器不记得你上一次是接口谁,所以每次请求都得自带身份凭证。实例最原始的安全做法是把用户名密码塞进 header,比如 Authorization: Basic base64(user:pass)。验证这东西现在基本没人用了,接口因为 base64 只是编码不是加密,抓包直接解出来就是明文密码。我见过一个内部运维系统还在用 Basic Auth,配合自签证书跑在内网,勉强能接受,但一旦有 SSRF 漏洞就全完了。

稍微正经一点的做法是签名机制。客户端和服务端共享一个 secret,每次请求把 method、path、timestamp、body 的 hash 拼在一起,用 HMAC-SHA256 算个签名放进 header。服务端收到后按同样规则算一遍,对不上就拒绝。关键点在于 timestamp 要参与签名并且服务端校验时间窗口,比如 ±5 分钟,否则重放攻击随便打。我一般还会把 nonce 也加进去,服务端用 Redis 存已用过的 nonce,TTL 设成时间窗口的两倍,这样同一个请求包没法用第二次。

具体到代码层面,假设用 Node.js 写一个中间件。请求进来先看 header 里有没有 X-Signature、X-Timestamp、X-Nonce 这三个字段。缺任何一个直接 401。然后检查 timestamp 和当前时间差是否超过 300 秒,超了返回 408 或者 401 都行,看你想怎么语义化。接着拼接待签名字符串:

const signStr = `${ method}\n${ path}\n${ timestamp}\n${ nonce}\n${ bodyHash}`; const expected = crypto.createHmac('sha256', secret).update(signStr).digest('hex'); if (expected !== signature) return res.status(401).end();

bodyHash 是对请求体做 SHA256,GET 请求 body 为空就 hash 空字符串。这里有个坑:如果服务端框架自动解析了 JSON body,拿到的对象再 JSON.stringify 可能和客户端原始字符串不一致,空格、键顺序都会影响 hash。所以要么让客户端在 header 里直接带 body 的 hash,要么服务端保留 raw body。Express 里用 express.raw() 或者 body-parser 的 verify 回调拿到原始 buffer。

Nonce 校验别忘了。Redis 用 SET key value NX EX 600,如果返回 null 说明这个 nonce 已经用过了,直接拒绝。key 可以设计成 nonce:{ appId}:{ nonce},方便按应用隔离和批量清理。

还有一点容易忽略:secret 不能硬编码在客户端。移动端 App 可以被反编译,前端 JS 更是裸奔。所以这种签名方案主要用在服务端到服务端的通信,或者客户端先通过 OAuth 拿到短期 token,再用 token 派生签名密钥。纯客户端场景还是老老实实上 TLS + token 刷新,别自己造轮子。

最后提一个真实踩过的坑。有次调试发现签名一直对不上,查了半天是 URL 里的 query string 顺序问题。客户端按字母序拼的,服务端按接收顺序拼的,导致签名不一致。解决办法是双方约定统一按 key 的字典序排序后再拼接。这种细节不写进接口文档,联调时能折腾一整天。

分享到: