bigywater.tech
Spring · 104

PasswordEncoder:BCrypt 的 cost 与耗时,DelegatingPasswordEncoder

BCrypt 的 cost 设多少合适?{bcrypt} 这个前缀是干什么的?旧库里没有前缀的哈希怎么办?72 字节限制会造成什么?这篇在 Spring Security 7 上测 BCrypt 不同 strength 的耗时,再逐个验证 DelegatingPasswordEncoder 的行为。

274.6 ms
BCrypt strength 12 的 encode 中位耗时(strength 10 是 70.1 ms)
86 字节也 matches
一个前 72 字节相同、后面不同的 86 字节密码,仍能 matches 通过 72 字节密码的哈希
{bcrypt}
DelegatingPasswordEncoder 的前缀;没有前缀的哈希会抛 IllegalArgumentException

BCrypt 的 strength

实测 · 本机执行输出
  默认 strength = 10;同一个密码编码两次:$2a$10$2zXk4kuCDTlq85SuLXd4d.AII1ikOPyLjsIkfX0nDf0IuEY4ciY/2 与 $2a$10$j7Mp7N.JUp2qRIhx5hCxReZSwwdCaFkiskvRNO/WrfYVTVCfMJKOu;长度 60;两个值相同:false;都能 matches:true
  strength= 4:encode 中位     1.2 ms,matches 中位     1.2 ms,哈希前缀 $2a$04$
  strength= 6:encode 中位     4.8 ms,matches 中位     4.7 ms,哈希前缀 $2a$06$
  strength= 8:encode 中位    17.7 ms,matches 中位    17.8 ms,哈希前缀 $2a$08$
  strength=10:encode 中位    70.1 ms,matches 中位    69.8 ms,哈希前缀 $2a$10$
  strength=12:encode 中位   274.6 ms,matches 中位   282.3 ms,哈希前缀 $2a$12$
  strength=14:encode 中位  1105.2 ms,matches 中位  1082.7 ms,哈希前缀 $2a$14$
  strength 取值范围:
    strength=3:IllegalArgumentException:Bad strength
    strength=32:IllegalArgumentException:Bad strength

每加 2 耗时约 ×4;strength 12 每次校验约 275 ms CPU。

DelegatingPasswordEncoder

实测 · 本机执行输出
  encode("secret") = {bcrypt}$2a$10$ABga77euJ266Yktz8GEDau5UGkfb7I/1i9lzBHsM3x0KUKNnWUKaW
  matches 带前缀的各种哈希(同一个明文 secret):
    {noop} → matches=true,哈希长度 12
    {bcrypt} → matches=true,哈希长度 68
    {pbkdf2@SpringSecurity_v5_8} → matches=true,哈希长度 124
    {scrypt@SpringSecurity_v5_8} → matches=true,哈希长度 105
    {argon2@SpringSecurity_v5_8} → matches=true,哈希长度 125
  没有前缀的哈希(例如直接从旧库里读出来的 BCrypt):
    抛 IllegalArgumentException:Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.
    setDefaultPasswordEncoderForMatches(BCrypt) 之后 matches → true
  upgradeEncoding(是否需要用新参数重新编码):
    {bcrypt} strength 4 → true;{bcrypt} strength 10(默认)→ false;{noop} → true;{bcrypt} strength 12 → false

前缀选算法;无前缀要设默认编码器;upgradeEncoding 用来升级旧哈希。

边界

实测 · 本机执行输出
  只在第 72 个字节之后不同的两个密码:matches(72 字节原文, 哈希)=true,matches(86 字节、前 72 字节相同, 同一个哈希)=true
  encode(73 字节的密码):抛 IllegalArgumentException:password cannot be more than 72 bytes
  matches(null, 哈希) 成功
  matches("", 空密码的哈希):false
  matches("secret", "not-a-hash"):false

encode 拒绝超过 72 字节,matches 会忽略 72 字节之后;空密码和乱格式哈希不抛异常。

算法对比

实测 · 本机执行输出
  BCrypt 默认(strength 10):encode 中位 67.8 ms,matches 中位 67.6 ms,哈希长度 60
  Pbkdf2 defaultsForSpringSecurity_v5_8:encode 中位 38.8 ms,matches 中位 38.9 ms,哈希长度 96
  SCrypt defaultsForSpringSecurity_v5_8:encode 中位 112.5 ms,matches 中位 106.3 ms,哈希长度 77
  Argon2 defaultsForSpringSecurity_v5_8:encode 中位 16.6 ms,matches 中位 15.5 ms,哈希长度 97

四种默认参数都在几十毫秒量级,但强度不能直接比较。

分步演示

PasswordEncoder
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Security 7(spring-security-crypto,版本由 Spring Boot 4.0.8 的 BOM 管理);用 javac 编译、java 直接运行,依赖由 Maven 解析;数字每次运行会有小幅抖动。单个 Java 程序,没有启动 Spring 容器;各项耗时取 5 次(strength 13 以上取 3 次)中位数,先预热;Argon2 需要 BouncyCastle(bcprov-jdk18on)
    • 没有验证:调大各算法的内存、并行度、迭代次数后的成本;GPU 破解成本的对比;Argon2 的 Argon2id 参数选择指南;密码 pepper;在 Web 请求里 BCrypt 校验占用的 CPU 对吞吐的影响;JDK 之外的原生实现;strength 31 之类的极端值(会跑几个小时)
    京ICP备2026067502号-1京公网安备11010502063487号