PAYATHON 2026

OAuth2 能否为支付应用提供足够的安全性?

支付阿杰

结论

OAuth 2.0 可以用于支付系统授权,但仅靠 OAuth 2.0、Bearer Token 和 HTTPS,还不足以保护高价值支付。

OAuth 2.0 处理的是委托授权,也就是谁可以代表谁访问哪些资源。它默认不会为每个业务请求提供端到端签名,也无法单独解决交易重放、交易内容确认、重复扣款和客户端设备失陷等问题。

大额资金转移需要多层防护。除了正确验证 TLS 证书,还应使用短期令牌、发送方约束令牌、交易防重放和交易级签名。HMAC-SHA256 可以作为其中一层,但不能替代 TLS,也未必适合无法安全保存密钥的客户端。

HTTPS 证书不验证时会发生什么

客户端一旦关闭证书验证,攻击者就可能通过伪造证书或代理连接发起中间人攻击,读取访问令牌、篡改收款账户和金额,或者重放截获的请求。

这不是 OAuth 2.0 特有的缺陷,而是因为 TLS 的安全前提已被破坏。下面的代码会让 HTTPS 基本失去验证服务器身份的能力:

import requests

# 错误:不要在生产环境关闭证书验证
response = requests.get(
    "https://api.example.com/account",
    verify=False,
)

客户端需要验证:

  • 证书链是否由受信任的 CA 签发;
  • 证书是否仍在有效期内;
  • 证书中的主机名是否与访问地址匹配;
  • TLS 验证失败时是否立即终止请求。

成熟的 HTTP 库通常会默认执行这些检查。不要因为测试证书或代理配置有问题,就在生产代码中设置 verify=False、跳过 hostname verification,或者安装“信任所有证书”的回调。

证书固定(certificate pinning)可以减少部分代理攻击风险,但也会增加证书轮换和灾难恢复的复杂度。是否采用,需要根据客户端的可控程度和运维能力判断。它不能替代正常的证书链验证。

OAuth 2.0 是否容易受到重放攻击

要根据被重放的对象分别判断。

授权码通常有效期很短,而且只能使用一次。结合 PKCE、严格的 redirect_uri 校验和 state 参数,可以大幅降低授权码被截获后重放的风险。

普通 Bearer Access Token 遵循“谁持有,谁就能使用”的模式。如果令牌因日志泄露、恶意软件、中间人攻击或错误配置而暴露,攻击者通常可以在令牌过期前反复使用。因此,高风险接口不应只依赖长期 Bearer Token。

即使令牌没有被盗,付款请求也可能因网络超时而被客户端重试。服务器可能已经完成扣款,只是响应在传输途中丢失。如果客户端再次提交请求,就可能造成重复扣款。这属于业务层的重放或重复执行,需要用幂等机制处理,OAuth 2.0 本身解决不了。

支付系统应如何加固

1. 强制正确的 TLS 验证

支付 API 只能通过 HTTPS 访问,不能回退到明文 HTTP。服务端应使用当前安全策略仍然接受的 TLS 配置,客户端也不能忽略证书错误。

下面的示例保留了证书验证,并通过幂等键提交付款:

import uuid
import requests

payment = {
    "amount": "1250.00",
    "currency": "CNY",
    "payee_id": "merchant-4821",
}

response = requests.post(
    "https://api.example.com/payments",
    headers={
        "Authorization": "Bearer ACCESS_TOKEN",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json=payment,
    timeout=10,
)

response.raise_for_status()
print(response.json())

requests 默认验证服务器证书。生产环境还要明确设置连接超时、读取超时和重试策略,不能对付款请求进行无条件自动重试。

2. 使用短期、最小权限的访问令牌

访问令牌应遵循这些原则:

  • 有效期尽可能短;
  • scope 仅允许必要操作;
  • 限制令牌受众,并校验 aud;
  • 服务端校验签发者、过期时间和权限;
  • Refresh Token 应轮换,并支持撤销;
  • 不把令牌写入 URL、日志、错误报告或分析系统。

只能“查看账户”的令牌,不应自动获得“发起转账”的权限。大额支付还可以要求用户再次授权或重新认证。

3. 使用发送方约束令牌

高风险接口可以采用以下任一种机制,将访问令牌绑定到特定客户端密钥:

  • Mutual TLS(mTLS)绑定的访问令牌;
  • DPoP(Demonstrating Proof of Possession)。

这样,即使访问令牌单独泄露,攻击者没有对应的私钥,通常也无法使用它。

使用 mTLS 时,请求大致如下:

import uuid
import requests

response = requests.post(
    "https://api.example.com/payments",
    headers={
        "Authorization": "Bearer ACCESS_TOKEN",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "amount": "1250.00",
        "currency": "CNY",
        "payee_id": "merchant-4821",
    },
    cert=("/secure/client.crt", "/secure/client.key"),
    verify="/secure/ca-bundle.pem",
    timeout=10,
)

response.raise_for_status()

重点不只是让请求携带客户端证书。授权服务器签发的访问令牌必须绑定到该证书,资源服务器也必须验证这一绑定关系。否则,客户端证书和访问令牌仍是两份相互独立的凭据。

实际部署时,支付机构通常不会把私钥作为普通 PEM 文件保存在磁盘上,而会使用 HSM、系统密钥库或其他不可导出的密钥存储。

4. 对支付操作实施业务级防重放

每笔付款都应携带不可预测的幂等键或唯一交易号。服务端必须将它与调用方、请求内容和处理结果关联起来。

典型的处理逻辑如下:

key = (client_id, idempotency_key)

if key 已存在:
    if 原请求摘要 != 当前请求摘要:
        返回 409 Conflict
    else:
        返回第一次处理的结果
else:
    在数据库事务中:
        保存 key 和请求摘要
        执行付款
        保存最终结果

客户端生成幂等键后,服务端不能只接收而不处理,还需要:

  • 设置合理的去重记录保留时间;
  • 防止同一个键对应不同金额或收款人;
  • 持久化幂等记录和付款状态;
  • 正确处理超时、并发请求和事务回滚。

时间戳和随机数也能用于防重放,但服务器必须检查允许的时间窗口,并保存已经使用过的随机数。只给请求添加时间戳并不能阻止重放。

5. 必要时对交易内容签名

如果需要证明“客户端确认了具体金额和收款人”,可以采用标准化的交易签名机制,例如 JWS,或者由协议明确规定的请求对象签名。

签名内容至少应覆盖:

{
  "payment_id": "pay-20260901-00042",
  "amount": "1250.00",
  "currency": "CNY",
  "payee_id": "merchant-4821",
  "issued_at": 1788230400,
  "nonce": "7d662588-58d2-45ef-bf43-f4715e795f25"
}

签名验证成功后,服务端仍要检查:

  • payment_id 或 nonce 是否已经使用;
  • issued_at 是否处于允许的时间窗口;
  • 签名密钥是否属于该调用方;
  • 金额、币种和收款人是否与实际执行内容完全一致;
  • 签名算法是否在允许列表中。

不要自行设计“拼接几个字段后执行 HMAC”的格式。字段顺序、字符编码、空值、数字格式或 JSON 规范化方式稍有差异,都可能导致签名不一致,甚至出现验证绕过。应优先采用成熟且明确规定序列化方式的标准。

HMAC-SHA256 是否更安全

不能简单判断 HMAC-SHA256 和 OAuth 2.0 哪个更安全,因为它们处理的不是同一类问题。

HMAC 可以验证消息完整性,并确认发送方持有共享密钥,但它本身:

  • 不会加密请求内容;
  • 不负责验证服务器证书;
  • 如果没有时间戳、随机数和服务端去重,仍然可能被重放;
  • 要求安全分发和保存共享密钥;
  • 无法区分使用同一共享密钥的多个参与方;
  • 不适合由浏览器、移动应用等公共客户端长期保存高价值密钥。

如果把 HMAC 密钥内置在移动应用中,攻击者通常可以通过逆向、调试或控制设备将其提取出来。此时,即使请求带有 HMAC 签名,也不能证明它来自可信用户或可信设备。

后端之间的支付接口更适合使用非对称密钥、mTLS、private_key_jwt 或协议规定的数字签名。采用非对称方案时,客户端只持有私钥,服务端保存公钥,在密钥轮换、审计和责任区分方面也更方便。

推荐的整体方案

大额支付通常需要一套组合方案:

  1. 使用 OAuth 2.0 或 OpenID Connect 处理用户身份和委托授权;
  2. 通过 PKCE、严格的重定向地址和一次性授权码保护授权流程;
  3. 使用 TLS 正确验证服务器身份,必要时启用 mTLS;
  4. 使用短期、最小权限且限制受众的访问令牌;
  5. 通过 mTLS 或 DPoP 将令牌绑定到客户端密钥;
  6. 为每笔支付设置幂等键和唯一交易号,并由服务端执行去重;
  7. 明确确认金额、币种和收款人等交易内容,必要时对其签名;
  8. 对大额或异常交易要求重新认证、多因素认证或人工复核;
  9. 完整记录授权、签名验证、风控决策和资金状态,但不记录敏感令牌;
  10. 根据所在司法管辖区、支付网络和监管要求选择适用的安全配置。

问题中的部分担忧是成立的。普通 Bearer Token 确实可能被重放,错误的证书验证也会带来严重风险。但这不代表 OAuth 2.0 不适合支付,只说明它负责的是授权层。高价值支付还需要令牌持有证明、交易防重放、内容确认、风控和密钥保护机制。

备注:内容仅供参考。