不能用 == 比较密码哈希或 token,因其存在计时攻击风险;应使用 crypto/subtle.constanttimecompare 进行常量时间比较,但需确保两字节切片长度一致且不提前校验长度。

为什么不能用 == 比较密码哈希或 token?
直接用 == 比较两个字节切片(比如 []byte)时,Go 会从左到右逐字节比对,一旦发现不等就立即返回 false。攻击者通过精确测量响应时间差异,能反推出密钥或 token 的前缀——这就是计时攻击。哪怕差异只有几十纳秒,在网络可控、请求量足够时也足以被统计放大。
ConstantTimeCompare 的使用前提和限制
crypto/subtle.ConstantTimeCompare 只接受两个 []byte,且要求长度相等;如果长度不同,它直接返回 false,**但这个判断是常量时间的**——不会因长度差而提前退出。这点很关键:你不能先用 len(a) != len(b) 做校验,否则又暴露了长度信息。
- 必须确保两个切片长度一致,否则函数返回
false,但你无法区分是“长度不等”还是“内容不等” - 只适用于字节序列比较,不支持字符串直接传入(需先转
[]byte,且注意字符串不可变性不影响安全性) - 不处理编码差异(如 base64 vs raw bytes),比较前必须统一格式
典型误用:先检查长度再调用 ConstantTimeCompare
下面这段代码看似合理,实则破坏了防护效果:
if len(got) != len(expected) {
return false // ⚠️ 这里就泄露了长度!
}
return subtle.ConstantTimeCompare(got, expected) == 1
正确做法是让长度差异也走常量时间路径。常见方案是:用固定长度填充或截断(如所有 token 统一为 32 字节),或用掩码方式抹平长度判断:
// 安全写法:不暴露长度差异
if len(got) != len(expected) {
// 仍调用 ConstantTimeCompare,但用相同长度的 dummy 数据占位
// 或更简单:统一长度(推荐在业务层保证)
return false
}
// 此时长度已一致,可安全调用
return subtle.ConstantTimeCompare(got, expected) == 1
真正安全的前提,是你的协议/存储本身已约定好固定长度(如 HMAC-SHA256 输出恒为 32 字节)。否则,你需要在上层做 padding(如 append(expected[:0], expected...) 补零),但补多少、怎么补必须对所有输入一致。
和 bcrypt / scrypt 等密码哈希的关系
ConstantTimeCompare 不替代密码哈希函数本身的安全性。它只解决「比对环节」的计时侧信道。如果你用 bcrypt.CompareHashAndPassword,它内部已经做了常量时间比较——**不需要也不应该再套一层 ConstantTimeCompare**。
- 对原始哈希值(如
sha256.Sum256输出的 32 字节)做校验时,才需要ConstantTimeCompare - 对 JWT signature、HMAC 验证结果、API token 等二进制凭证做比对时,适用
- 对用户输入的明文密码做比对?绝对不行——必须先哈希再比对,且哈希过程本身要加盐、慢迭代
最易忽略的一点:ConstantTimeCompare 返回的是 int(0 或 1),不是 bool,漏写 == 1 会导致逻辑反转。











