HTTP请求结合HMAC增加安全性

前言

本文主要是分享一个挺不错的,可行的,结合 hmac 增加 http 请求安全性的方案。

🍃 小贴士: HMAC 之前我写过文章了,不了解的小伙伴可以去看看。

小目标:看完之后要能用自己的语言描述一遍 HMAC 签名方案的设计思路。

正文

问题背景

在传统的 HTTP 请求中,我们常常面临这样的安全挑战问题:

  • 数据篡改:攻击者可以在传输过程中修改请求内容
  • 重放攻击:合法的请求被恶意重复发送
  • 身份伪造:无法确认请求的真正来源

虽然 HTTPS 解决了传输过程中的加密问题,但它无法防止【重放攻击】,也无法验证业务数据的【完整性】。

🤔 为什么?

✍️ 因为 HTTPS 只保障传输过程的安全,而【重放攻击】和【业务数据完整性】是针对已接收的合法报文进行的攻击,需要在应用层通过时序戳、流水号、数字签名等额外机制来防护。

补充知识

在网络通信的七层或四层(TCP/IP)模型中,应用层位于最顶层,直接为用户的应用程序提供网络服务(如 HTTP、API 接口),其下方是负责可靠传输的传输层(如 TCP/TLS)和负责寻址路由的网络层等。

HTTPS 是在传输层之上对通信管道进行加密和身份认证,它确保了数据在传输过程中不被窃听或篡改。然而,一旦数据被目标服务器正确接收,HTTPS 的使命就结束了,它无法辨别一个被完整接收的请求是来自用户的"一次合法操作",还是攻击者截获后重放的重复请求,也无法验证业务逻辑参数(如订单金额、用户 ID)在应用层逻辑中是否被恶意篡改。

因此,必须在应用层设计额外的安全机制,如签名、时效验证和非重复令牌,来防御此类针对业务逻辑的攻击。

HMAC 签名方案设计

下面介绍一个基于 【HMAC】 的 HTTP 请求签名方案,能有效解决上述问题:

核心思路

每个 HTTP 请求都携带一个基于请求内容和共享密钥计算出的 HMAC 签名,服务端通过验证签名来确保请求的合法性和完整性。

签名生成步骤

  1. 准备签名要素

    • 时间戳(防止重放攻击)
    • 随机数(进一步增强唯一性)
    • 请求方法(GET/POST/PUT/DELETE)
    • 请求路径
    • 请求参数(URL 参数 + Body 参数)
    • 共享密钥(服务端和客户端预先约定)
  2. 构造待签名字符串

    text 复制代码
    待签名字符串 = 时间戳 + "|" + 随机数 + "|" + 请求方法 + "|" + 请求路径 + "|" + 排序后的参数字符串
  3. 计算 HMAC 签名

    javascript 复制代码
    signature = crypto.createHmac('sha256', secretKey).update(待签名字符串).digest('hex')

请求头设置

将签名相关信息放入 HTTP 头中:

http 复制代码
X-Auth-Timestamp: 1697012400000
X-Auth-Nonce: abc123def456
X-Auth-Signature: 7a8f9b3c4d5e6f...

服务端验证流程

服务端收到请求后的验证过程:

  1. 基础检查

    • 检查时间戳是否在允许的时间窗口内(如 ±5 分钟)
    • 检查随机数是否在近期使用过(防止重放),这个一般如何实现呢?一个可行的操作:服务端维护一个最近使用的随机数缓存,每次验证时检查随机数是否已存在于缓存中。如果存在,拒绝请求;如果不存在,将随机数加入缓存。
  2. 重新计算签名

    • 按照相同的规则构造待签名字符串
    • 使用相同的密钥计算 HMAC
  3. 签名比对

    • 比较计算出的签名与请求头中的签名是否一致
    • 一致则通过验证,不一致则拒绝请求

实际应用示例

以下是一个 Node.js 的简单实现:

javascript 复制代码
// 客户端签名生成
function generateSignature(secretKey, method, path, params, timestamp, nonce) {
    // 对参数按key排序并序列化
    const sortedParams = Object.keys(params)
        .sort()
        .map(key => `${key}=${params[key]}`)
        .join('&')

    const stringToSign = `${timestamp}|${nonce}|${method}|${path}|${sortedParams}`

    return crypto.createHmac('sha256', secretKey).update(stringToSign).digest('hex')
}

// 服务端验证
function verifySignature(request, secretKey) {
    const {headers, method, path, query, body} = request
    const timestamp = headers['x-auth-timestamp']
    const nonce = headers['x-auth-nonce']
    const receivedSignature = headers['x-auth-signature']

    // 检查时间窗口
    if (Math.abs(Date.now() - parseInt(timestamp)) > 5 * 60 * 1000) {
        return false // 超过5分钟
    }

    // 检查随机数是否重复(需要维护一个近期nonce缓存)
    if (nonceCache.has(nonce)) {
        return false
    }
    nonceCache.add(nonce)

    // 合并所有参数
    const allParams = {...query, ...body}
    const calculatedSignature = generateSignature(secretKey, method, path, allParams, timestamp, nonce)

    return crypto.timingSafeEqual(Buffer.from(calculatedSignature, 'hex'), Buffer.from(receivedSignature, 'hex'))
}

这个方案的优势

  1. 防篡改:任何对请求内容的修改都会导致签名验证失败
  2. 防重放:时间戳 + 随机数机制有效防止请求被重复使用
  3. 身份认证:只有拥有正确密钥的客户端才能生成有效签名
  4. 性能高效:HMAC 计算相比非对称加密性能更好
  5. 易于实现:方案简单,各种编程语言都有现成的 HMAC 库

注意事项

  • 密钥管理:密钥需要安全存储和定期轮换
  • 时间同步:确保客户端和服务端时间基本同步
  • 随机数存储:服务端需要维护一个短期的随机数缓存
  • 敏感信息:即使有签名保护,敏感数据仍应通过 HTTPS 传输

最后

这个 HMAC 签名方案在实际项目中表现相当不错,既保证了安全性,又不会对性能造成太大影响。特别适合用在内部 API、微服务间调用等需要较强身份认证和防篡改的场景。

当然,没有绝对的安全方案,安全不是一劳永逸的事情,世界上没有绝对安全的系统,只能通过不断的完善和升级来提高安全等级。

这边回答下文章开头的目标:用自己的语言描述一遍 HMAC 签名方案的设计思路。

【✍️ 回答】HMAC 签名方案通过在请求中包含基于请求内容和共享密钥计算出的签名,服务端通过验证签名来确保请求的合法性和完整性。

相关链接

相关推荐
武子康2 小时前
大数据-113 Flink 源算子详解:非并行源(Non-Parallel Source)的原理与应用场景
大数据·后端·flink
QZQ541882 小时前
高性能现代CPP--表达式模板(expression templates)
后端
aidingni8882 小时前
掌握 TCJS 游戏摄像系统:打造动态影院级体验
前端·javascript
有梦想的攻城狮2 小时前
从0开始学vue:npm命令详解
前端·vue.js·npm
我是日安3 小时前
从零到一打造 Vue3 响应式系统 Day 23 - Watch:基础实现
前端·javascript·vue.js
莹Innsane3 小时前
使用 VictoriaLogs 存储和查询服务器日志
后端
FogLetter3 小时前
TypeScript 泛型:让类型也拥有“函数式”超能力
前端·typescript
karry_k3 小时前
BlockingQueue与SynchronousQueue
后端
FogLetter3 小时前
Map 与 WeakMap:内存管理的艺术与哲学
前端·javascript