3、令牌绑定技术

令牌绑定,说白了就是给令牌加一把“物理锁”。

我早年做支付系统的时候,遇到过一件挺头疼的事:用户的 access token 被中间人截获了,攻击者拿着这个 token 就能冒充用户做任何操作。那时候我们用的还是纯 Bearer Token,谁拿到谁就能用。后来我才意识到,问题出在令牌和客户端之间没有绑定关系。

令牌绑定的核心思想很简单:令牌只在特定的安全上下文中有效。这个上下文可以是 TLS 连接、Channel ID,或者通过专门的 Token Binding 协议来建立。一旦令牌离开了它绑定的环境,就变成了一张废纸。

3.1 令牌绑定概念

令牌绑定(Token Binding)是一种安全机制,它把令牌和传输层的某些特征绑定在一起。攻击者即使偷走了令牌,也无法在其他设备或连接上使用它。

我习惯把令牌绑定分成三个层次:

  • 弱绑定:令牌和客户端 IP 地址绑定。这招在 NAT 环境下基本失效,我不推荐。
  • 中绑定:令牌和 TLS 会话或 Channel ID 绑定。这是目前业界的主流做法。
  • 强绑定:通过 Token Binding 协议,在应用层建立加密绑定。这是最安全的方案。

核心原则:令牌的签发和验证必须基于同一个绑定密钥。签发时记录绑定信息,验证时检查绑定信息是否匹配。

3.2 Channel ID 绑定

Channel ID 是 Google 在 OAuth 2.0 中提出的一种绑定方案。它利用 TLS 客户端证书来标识一个安全的通道。

我记得第一次实现 Channel ID 时,踩了个坑:客户端证书的生成和管理是个大麻烦。每个客户端都需要生成自己的密钥对,然后把公钥注册到服务器。这听起来简单,但在移动端做起来特别痛苦。

Channel ID 的工作流程大致如下:

  1. 客户端生成一个 RSA 密钥对(2048 位以上)。
  2. 客户端把公钥通过 TLS 握手发送给服务器。
  3. 服务器计算 Channel ID = SHA256(公钥)。
  4. 服务器在签发令牌时,把 Channel ID 嵌入令牌中。
  5. 后续请求中,客户端必须使用对应的私钥签名请求。
  6. 服务器验证签名,并比对令牌中的 Channel ID 是否匹配。

注意:Channel ID 要求客户端必须支持 TLS 扩展。在老旧浏览器或某些嵌入式设备上,这个方案可能行不通。

3.3 TLS 会话绑定

TLS 会话绑定比 Channel ID 更轻量。它不需要客户端证书,而是利用 TLS 会话的某些特征来建立绑定。

我个人比较喜欢用 TLS 会话 ID 或 TLS 会话票证(Session Ticket)来做绑定。为什么呢?因为这些信息在 TLS 握手中就已经存在了,不需要额外的协议开销。

具体做法是这样的:

  • 服务器在 TLS 握手完成后,获取会话 ID 或会话票证。
  • 把这个信息作为绑定因子,嵌入到令牌中。
  • 每次请求时,服务器检查当前 TLS 会话是否和令牌中的绑定信息一致。

这里有个坑:TLS 会话可能会过期或被重用。我曾经遇到过一个问题:负载均衡器把同一个用户的请求分发到了不同的后端服务器,导致 TLS 会话 ID 不一致,令牌验证失败。解决方案是使用会话票证,并在所有服务器之间共享票证密钥。

绑定方式 绑定因子 优点 缺点
Channel ID 客户端公钥 强安全、不依赖 TLS 实现 需要客户端证书管理
TLS 会话 ID 会话 ID 实现简单、零额外开销 负载均衡场景下容易失效
TLS 会话票证 票证内容 支持跨服务器共享 票证密钥管理复杂

3.4 Token Binding 协议

Token Binding 协议是 IETF 的标准方案(RFC 8471、RFC 8472)。它解决了前面两种方案的共同痛点:没有统一的标准。

Token Binding 协议的核心是:在 TLS 握手阶段,客户端和服务器协商一个 Token Binding 密钥。这个密钥由 TLS 的导出密钥材料(Exported Keying Material, EKM)生成。

嗯,这里要注意:Token Binding 协议不是取代 TLS,而是在 TLS 之上增加一层绑定关系。

协议流程如下:

  1. 客户端在 TLS 握手中发送 token_binding 扩展。
  2. 服务器确认支持 Token Binding,并返回自己的参数。
  3. 双方基于 TLS EKM 计算出 Token Binding 密钥。
  4. 客户端使用这个密钥对令牌进行签名。
  5. 服务器验证签名,确保令牌和当前 TLS 连接绑定。

经验之谈:Token Binding 协议在浏览器环境中支持度还不够好。如果你做的是原生应用或服务端之间的通信,可以考虑使用。Web 场景下,我建议先用 TLS 会话绑定过渡。

Token Binding 协议定义了两种绑定模式:

  • Provided Token Binding:客户端提供绑定密钥,服务器验证。
  • Referred Token Binding:一个服务把令牌传递给另一个服务,后者验证绑定关系。

第二种模式在微服务架构中特别有用。比如,认证服务签发了令牌,业务服务验证令牌时,可以检查 Token Binding 是否一致。

// Token Binding 验证示例(伪代码)
function validateTokenBinding(token, tlsConnection) {
    // 从令牌中提取绑定的密钥标识
    const bindingKeyId = token.getBindingKeyId();
    
    // 从 TLS 连接中获取当前的 Token Binding 密钥
    const currentKey = tlsConnection.getTokenBindingKey();
    
    // 比对密钥是否一致
    if (bindingKeyId !== currentKey.id) {
        throw new Error("Token Binding 不匹配");
    }
    
    // 验证令牌签名
    if (!token.verifySignature(currentKey.publicKey)) {
        throw new Error("令牌签名验证失败");
    }
    
    return true;
}

关键点:Token Binding 协议把绑定密钥的生成和验证都标准化了。你不需要自己设计密钥派生逻辑,协议已经帮你做好了。

下面这张图展示了三种令牌绑定技术的核心差异和适用场景:

令牌绑定技术对比 Channel ID 绑定 TLS 会话绑定 Token Binding 协议 特征 • 基于客户端证书 • 强安全但管理复杂 • 适合企业级应用 特征 • 基于会话 ID/票证 • 实现简单零开销 • 适合 Web 场景 特征 • 基于 TLS EKM • 标准化方案 • 适合微服务架构 适用场景对比 高安全需求 金融、政务系统 内部网络环境 快速部署 Web 应用、API 中小型项目 标准化通信 微服务、跨域 原生应用

三种技术各有千秋。我个人建议:如果你从零开始搭建系统,优先考虑 Token Binding 协议。如果系统已经上线,想快速加固,TLS 会话绑定是最快的选择。Channel ID 嘛,除非你有强制性的客户端证书管理需求,否则不太推荐。

避坑指南:我曾经在一个项目中同时使用了 Channel ID 和 TLS 会话绑定,结果发现两者互相干扰。记住,绑定方式只能选一种,不要混用。混用会导致验证逻辑复杂化,而且容易引入安全漏洞。