4、令牌传输安全:HTTPS强制、绑定、防止重放攻击
令牌在传输过程中被截获,是所有安全攻击里最常见的一种。我见过太多团队把精力花在签名算法上,结果令牌在公网裸奔,一把抓走。说白了,传输安全是最后一道防线——你令牌设计得再精巧,如果传输链路是明文的,那一切等于零。
这一章我们聚焦三个核心问题:怎么保证链路可信、怎么把令牌和客户端绑定、怎么让截获的令牌无法二次使用。
4.1 HTTPS强制:不是选项,是底线
我个人习惯把HTTPS强制列为令牌治理的第一条军规。为什么?因为HTTP是明文,令牌在中间节点可以被任何人读取。你想想看,一个中间人攻击(MITM)就能让你的OAuth流程形同虚设。
正确的做法是:全站HTTPS,且强制HSTS。HSTS(HTTP Strict Transport Security)告诉浏览器:这个域名只允许HTTPS访问,连第一次HTTP重定向都不允许。
我曾经在一个金融项目中遇到过:客户坚持部分静态资源走HTTP以节省CDN成本。结果安全审计时发现,攻击者通过篡改HTTP响应的JS文件,直接窃取了所有用户的访问令牌。嗯,从那以后,我再也不接受「部分HTTPS」的方案。
HSTS配置示例
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
参数说明:
- max-age:单位秒,31536000即一年。建议至少半年。
- includeSubDomains:所有子域名也强制HTTPS。
- preload:提交到浏览器预加载列表,彻底杜绝第一次HTTP请求。
4.2 Binding:把令牌锁在客户端
HTTPS解决了链路加密问题,但解决不了「令牌被完整复制后在其他设备使用」的问题。这就是 Binding要干的事。
Binding的核心思想很简单:把令牌和客户端的TLS连接绑定。客户端在TLS握手时生成一个公私钥对,服务端把公钥的哈希值嵌入令牌。每次请求时,客户端用私钥签名,服务端验证签名是否匹配。
说白了,令牌离开了原来的TLS连接,就无法通过验证。攻击者即使截获了令牌,也无法在其他设备上使用。
Binding vs 传统Bearer
| 特性 | 传统Bearer | Binding |
|---|---|---|
| 传输方式 | 仅依赖HTTPS | HTTPS + TLS层绑定 |
| 令牌可移植性 | 可被复制到任意设备 | 绑定到特定TLS连接 |
| 重放攻击防护 | 需额外机制 | 天然防护 |
| 实现复杂度 | 低 | 中高 |
| 浏览器支持 | 全支持 | 有限(需扩展) |
我记得有一次帮一个银行客户做架构评审,他们用的是传统Bearer ,结果测试团队在WiFi环境下成功截获令牌并重放。后来我们引入了 Binding,虽然改造工作量不小,但安全等级直接上了一个台阶。
4.3 防止重放攻击:时间戳 + Nonce + 签名
即使有了HTTPS和 Binding,重放攻击依然可能发生——比如攻击者通过内存dump获取了令牌,在令牌过期前发起请求。怎么办?
我的做法是三重防护:时间戳校验 + Nonce一次性随机数 + 请求签名。
4.3.1 时间戳校验
客户端在请求头中携带时间戳,服务端判断是否在允许的时间窗口内(通常±5分钟)。超出窗口的请求直接拒绝。
X-Timestamp: 1712345678
服务端校验逻辑:
if abs(current_timestamp - request_timestamp) > 300:
return 401 Unauthorized
4.3.2 Nonce一次性随机数
时间戳只能防「超时重放」,防不了「窗口内重放」。Nonce就是解决这个问题的——每个请求携带一个唯一随机数,服务端记录已使用的Nonce,重复使用直接拒绝。
X-Nonce: a1b2c3d4-e5f6-7890-abcd-ef1234567890
4.3.3 请求签名
时间戳和Nonce如果被篡改怎么办?签名就是最后一道锁。客户端用密钥对「请求方法 + 路径 + 请求体 + 时间戳 + Nonce」进行签名,服务端用同样的方式验证。
我曾经在某个IoT项目中遇到过:设备端时间不同步,导致时间戳校验频繁失败。后来我们改用「服务端下发时间戳 + 客户端签名」的方式,既解决了时间同步问题,又保证了防重放能力。
完整请求示例
POST /api/transfer HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
X-Timestamp: 1712345678
X-Nonce: a1b2c3d4-e5f6-7890-abcd-ef1234567890
X-Signature: 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7
{"amount": 1000, "to": "user_456"}
4.4 整体架构图:令牌传输安全防护体系
下面这张图展示了从客户端到服务端的完整防护链路。我建议你把它贴在团队的白板上,每次设计新接口时对照检查。
4.5 避坑指南:我踩过的三个坑
做传输安全这么多年,有些坑是反复出现的。我列三个最常见的,你遇到了可以直接抄答案。
- 时间戳精度问题:我曾经用秒级时间戳,结果高并发下多个请求在同一秒内,Nonce还没来得及去重就被重放了。后来改成毫秒级时间戳 + 更严格的Nonce生成策略,问题解决。
- Nonce存储膨胀:如果不设置TTL,Nonce表会无限增长。我的做法是用Redis的EXPIRE命令,TTL设置为时间窗口的两倍(比如10分钟),既保证安全又控制存储。
- Binding的浏览器兼容性:目前主流浏览器对 Binding的原生支持有限。我的折中方案是:在移动端App中强制使用 Binding,Web端用「指纹 + 签名」作为替代方案。