将前端生成的"令牌"(即数据哈希或签名)做成动态的,是提高其安全性和有效性的一种常用手段。
静态哈希(仅对提交的数据本身做哈希)的主要弱点在于:
- 可预测性: 如果攻击者知道哈希算法和数据格式,他们可以在修改数据后,重新计算出对应的"正确"哈希值,从而绕过服务端的校验(前提是攻击者能控制前端 JS 执行)。
- 重放攻击 (Replay Attack): 攻击者可以截获一个合法的请求(包含数据和对应的静态哈希),然后在稍后原封不动地重新发送这个请求,服务端校验哈希仍然会通过(尽管这可能需要配合其他机制如 Nonce 来防范)。
通过引入动态元素到哈希计算的数据中,可以显著增加攻击者伪造或重用哈希的难度。
如何实现动态化?
核心思想是在计算哈希/签名时,不仅仅包含要提交的核心业务数据,还要加入一些每次请求都不同 或与当前上下文相关的动态信息。
以下是一些常用的动态元素:
-
时间戳 (Timestamp):
-
做法: 在前端获取当前时间的毫秒级时间戳,将其包含在要哈希的数据字符串中。
javascript// 前端 const data = { userId: 123, amount: 100 }; const timestamp = Date.now().toString(); // 获取当前时间戳字符串 const dataToHash = JSON.stringify(data) + "|" + timestamp; // 将数据和时间戳拼接 const hash = sha256(dataToHash); // 计算哈希 // 发送请求时带上 data, timestamp, hash // 例如放在请求头: // X-Data-Hash: hash // X-Request-Timestamp: timestamp -
服务端校验:
- 接收
data,timestamp,hash。 - 检查
timestamp是否在可接受的时间窗口内(例如,与服务器当前时间相差不超过几秒或几十秒),防止过旧的请求被重放。 - 用完全相同 的方式拼接接收到的
data和timestamp。 - 重新计算哈希。
- 比较计算出的哈希和接收到的
hash是否一致。
- 接收
-
优点: 简单易实现,每次请求的哈希都不同。
-
缺点: 客户端和服务器需要有大致同步的时间;如果攻击者能控制 JS,他们可以发送当前的
timestamp和篡改后数据计算出的哈希。主要增加了重放攻击的难度。
-
-
Nonce (Number used once - 随机数):
-
做法:
- 服务器生成: 服务器在加载页面或通过特定 API 预先提供一个一次性的随机字符串 (Nonce)。前端在计算哈希时包含这个 Nonce。
- 前端生成 (不太推荐,除非服务器能有效跟踪): 前端生成一个足够随机且唯一的字符串。
javascript// 前端 (假设从服务器获取了 nonce) const data = { userId: 123, amount: 100 }; const nonce = getNonceFromServer(); // 获取一次性随机数 const dataToHash = JSON.stringify(data) + "|" + nonce; const hash = sha256(dataToHash); // 发送请求时带上 data, nonce, hash -
服务端校验:
- 接收
data,nonce,hash。 - 验证
nonce的有效性: 检查该nonce是否由服务器签发且从未使用过。使用过的 Nonce 应立即标记为无效。 - 拼接
data和nonce,重新计算哈希。 - 比较哈希值。
- 接收
-
优点: 非常有效地防止重放攻击,因为每个 Nonce 只能用一次。
-
缺点: 服务器需要生成、存储、验证和管理 Nonce 的状态,增加了服务端的复杂性。
-
-
结合密钥的 HMAC (Hash-based Message Authentication Code):
-
做法: 这本身就是一种动态化的方式,因为它引入了一个密钥 (Secret Key) 。但如前所述,将密钥安全地存储在前端非常困难。如果要做,可以考虑:
- 短期动态密钥: 服务器通过安全方式(例如在用户登录后,通过一个受保护的 API)下发一个有时效性的短生命周期密钥给前端,前端使用这个密钥计算 HMAC。密钥过期后需要重新获取。
javascript// 前端 (假设获取了 sessionKey) const data = { userId: 123, amount: 100 }; const sessionKey = getSessionKey(); // 获取当前会话的短期密钥 const dataString = JSON.stringify(data); const hmacSignature = hmacSha256(dataString, sessionKey); // 使用密钥计算 HMAC // 发送请求时带上 data, hmacSignature (可能还需要标识密钥版本/ID) -
服务端校验: 使用与前端相同的、对应当前会话/用户的密钥,对接收到的
data计算 HMAC,并与接收到的hmacSignature比较。 -
优点: 比简单哈希更难伪造,因为需要密钥。
-
缺点: 前端密钥管理是核心难题。如果攻击者能拿到 JS 中的密钥,HMAC 依然可以被伪造。动态密钥增加了复杂性。
-
-
挑战-应答 (Challenge-Response):
- 做法:
- 前端发起请求意图。
- 服务器生成一个随机的"挑战 (Challenge)"字符串,发送给前端。
- 前端将这个
challenge、要提交的data(可能还有时间戳或 Nonce)一起进行哈希或签名计算。 - 前端将
data、challenge和计算结果response发送给服务器。
- 服务端校验: 服务器查找之前发送给该会话的
challenge,用相同方式计算预期response,并与收到的response比较。 - 优点: 增加了交互,使得每次请求的计算都依赖于服务器的动态输入。
- 缺点: 增加了一次网络往返,流程更复杂。同样,如果 JS 被控制,攻击者可以正确响应挑战。
- 做法:
总结与建议:
- 动态化是必要的: 相比静态哈希,引入动态元素(如时间戳或 Nonce)可以显著提高安全性,特别是防御重放攻击。
- 时间戳 + 哈希: 实现简单,能有效防止旧请求重放,是常用的基础动态化方案。
- Nonce + 哈希: 防重放效果最好,但增加了服务器状态管理的复杂性。
- HMAC (带动态密钥): 理论上更安全,但前端密钥管理是巨大挑战,需要非常谨慎地设计密钥下发和轮换机制。
- 核心限制不变: 无论如何动态化,只要哈希/签名计算过程完全在前端(且攻击者能控制 JS),攻击者总是有可能通过模拟计算过程来伪造有效的"令牌"。这些前端校验手段的主要作用是增加攻击难度 和提供信号,而不是提供绝对的安全保证。
- 最终依赖: HTTPS 保证传输安全 + 严格的服务端验证(业务逻辑、权限、输入清理) 仍然是安全的基石。前端的动态哈希/签名应作为辅助增强手段。
选择哪种动态化方式取决于你的安全需求、对复杂性的接受程度以及服务端的实现能力。通常 时间戳 + 哈希 是一个不错的起点。