
密码哈希算法 — bcrypt 与 Argon2 详解一、为什么密码存储需要专用算法1.1 普通哈希的致命缺陷MD5/SHA-256 设计目标是快文件校验、签名需要高速。但用于密码存储时快反而是致命缺点攻击者拿到数据库泄露的哈希值后 MD5 速度~100亿次/秒GPU 6位纯数字密码有 100万种组合 暴力破解时间100万 ÷ 100亿 0.0001秒 SHA-256 速度~50亿次/秒GPU 8位小写字母密码有 2080亿种组合 暴力破解时间2080亿 ÷ 50亿 41秒结论普通哈希太快了攻击者能在极短时间内遍历所有可能的密码。1.2 密码哈希的核心设计理念故意设计得很慢让暴力破解的成本从0.0001秒变成几百年。bcrypt 速度~10次/秒相同硬件 6位纯数字密码100万 ÷ 10 100000秒 ≈ 28小时 8位混合密码几百年注博客https://blog.csdn.net/badao_liumang_qizhi二、bcrypt2.1 什么是 bcrypt基于 Blowfish 加密算法改造的密码哈希函数。1999年发布至今仍是最广泛使用的密码哈希方案。2.2 核心特性特性说明自适应代价因子可以调整计算轮数硬件变快就增加轮数内置盐值自动生成随机盐不需要手动管理固定输出格式输出包含算法标识代价因子盐哈希值最大输入限制只处理前72字节超长密码截断2.3 输出格式解析$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy │ │ │ │ │ │ │ │ │ │ │ └─ 哈希值31字符 │ │ │ │ └─ 盐值22字符Base64编码的16字节 │ │ │ └─ 代价因子10表示2^101024轮 │ │ └─ 分隔符 │ └─ 算法版本2a/2b/2y └─ 标识符2.4 代价因子Cost Factor代价因子决定计算复杂度每增加1计算时间翻倍代价因子计算轮数大约耗时适用场景416~1ms仅测试用101024~100ms普通Web应用推荐最低值124096~400ms安全要求较高1416384~1.5s高安全场景1665536~6s极高安全注意用户体验选择原则让一次哈希耗时在 100ms-500ms 之间。太快不安全太慢影响登录体验。2.5 代码示例importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;publicclassBcryptExample{// 创建编码器参数是代价因子默认10privatestaticfinalBCryptPasswordEncoderencodernewBCryptPasswordEncoder(12);/** * 注册时对密码进行哈希. */publicstaticStringhashPassword(StringrawPassword){Stringhashedencoder.encode(rawPassword);// 输出示例$2a$12$LJ3m4sMRpNeU8WpDU8h7tuQOBCKFPBRjPfOcg1vKLAvEjxV3mGCHareturnhashed;}/** * 登录时验证密码. */publicstaticbooleanverifyPassword(StringrawPassword,StringstoredHash){returnencoder.matches(rawPassword,storedHash);}publicstaticvoidmain(String[]args){StringpasswordmyPassword123;// 注册 — 生成哈希存入数据库Stringhash1hashPassword(password);Stringhash2hashPassword(password);// 同一密码每次结果不同盐不同System.out.println(哈希1: hash1);System.out.println(哈希2: hash2);System.out.println(两次哈希相同 hash1.equals(hash2));// false// 登录 — 验证System.out.println(验证正确密码: verifyPassword(myPassword123,hash1));// trueSystem.out.println(验证错误密码: verifyPassword(wrongPassword,hash1));// false}}2.6 为什么同一密码每次哈希结果不同hashPassword(123456)→$2a$12$ABC...xyz...// 第一次hashPassword(123456)→$2a$12$DEF...uvw...// 第二次完全不同因为 bcrypt每次自动生成随机盐嵌入结果中。验证时从存储的哈希值中提取盐重新计算后比对验证过程 1. 从存储的 $2a$12$ABC...xyz... 中提取盐 ABC... 2. 用提取的盐 输入的密码 → 重新计算哈希 3. 计算结果与存储的哈希值比对 4. 一致 → 密码正确2.7 bcrypt 的工作原理简化输入: 密码 盐 代价因子(n) 1. 用密码和盐初始化 Blowfish 密码的 key schedule 2. 循环 2^n 次 - 用密码重新加密 key schedule - 用盐重新加密 key schedule 3. 用最终的 key 加密固定明文 OrpheanBeholderScryDoubt重复64次 4. 输出加密结果作为哈希值 为什么慢 第2步循环 2^n 次每次都是完整的加密运算 为什么不能并行加速 每次循环依赖上一次的结果串行依赖三、Argon23.1 什么是 Argon22015年 Password Hashing Competition 的冠军算法。设计目标抵抗 GPU/FPGA/ASIC 等专用硬件的暴力破解。3.2 bcrypt 的不足bcrypt 只消耗 CPU 时间而现代攻击者有大量 GPUCPU串行计算一个核心一次算一个 GPU数千个核心并行虽然单个慢但总量大 ASIC专用芯片专门设计来计算bcrypt bcrypt 只需 4KB 内存 → GPU/ASIC 可以轻松并行数千个实例3.3 Argon2 的解决方案消耗内存Argon2 设计思路 - 不仅消耗 CPU 时间还消耗大量内存 - GPU 核心多但每个核心内存小 → 无法并行太多实例 - ASIC 造内存比造计算单元贵得多 → 提高攻击成本3.4 三个变体变体防御目标适用场景Argon2d抵抗 GPU/ASIC数据依赖访存后端密码哈希、加密货币Argon2i抵抗侧信道攻击数据无关访存密钥派生Argon2id混合先i后d通用推荐兼顾两者3.5 核心参数参数含义推荐值内存代价memory使用多少 KB 内存65536 KB64MB时间代价iterations计算轮数3并行度parallelism使用几个线程4输出长度哈希值字节数32字节盐长度随机盐字节数16字节3.6 代码示例importorg.bouncycastle.crypto.generators.Argon2BytesGenerator;importorg.bouncycastle.crypto.params.Argon2Parameters;importjava.security.SecureRandom;importjava.util.Base64;publicclassArgon2Example{privatestaticfinalintMEMORY65536;// 64MBprivatestaticfinalintITERATIONS3;// 3轮privatestaticfinalintPARALLELISM4;// 4线程privatestaticfinalintHASH_LENGTH32;// 32字节输出privatestaticfinalintSALT_LENGTH16;// 16字节盐/** * Argon2id 哈希密码. */publicstaticStringhashPassword(Stringpassword){// 生成随机盐byte[]saltnewbyte[SALT_LENGTH];newSecureRandom().nextBytes(salt);// 配置参数Argon2ParametersparamsnewArgon2Parameters.Builder(Argon2Parameters.ARGON2_id).withMemoryAsKB(MEMORY).withIterations(ITERATIONS).withParallelism(PARALLELISM).withSalt(salt).build();// 计算哈希Argon2BytesGeneratorgeneratornewArgon2BytesGenerator();generator.init(params);byte[]hashnewbyte[HASH_LENGTH];generator.generateBytes(password.toCharArray(),hash);// 编码为存储格式$argon2id$v19$m65536,t3,p4$salt$hashStringsaltBase64Base64.getEncoder().withoutPadding().encodeToString(salt);StringhashBase64Base64.getEncoder().withoutPadding().encodeToString(hash);returnString.format($argon2id$v19$m%d,t%d,p%d$%s$%s,MEMORY,ITERATIONS,PARALLELISM,saltBase64,hashBase64);}/** * 验证密码. */publicstaticbooleanverifyPassword(Stringpassword,StringstoredHash){// 解析存储的哈希值中的参数和盐String[]partsstoredHash.split(\\$);// parts[3] m65536,t3,p4// parts[4] salt (Base64)// parts[5] hash (Base64)String[]paramPartsparts[3].split(,);intmemoryInteger.parseInt(paramParts[0].substring(2));intiterationsInteger.parseInt(paramParts[1].substring(2));intparallelismInteger.parseInt(paramParts[2].substring(2));byte[]saltBase64.getDecoder().decode(parts[4]);byte[]expectedHashBase64.getDecoder().decode(parts[5]);// 用相同参数重新计算Argon2ParametersparamsnewArgon2Parameters.Builder(Argon2Parameters.ARGON2_id).withMemoryAsKB(memory).withIterations(iterations).withParallelism(parallelism).withSalt(salt).build();Argon2BytesGeneratorgeneratornewArgon2BytesGenerator();generator.init(params);byte[]computedHashnewbyte[expectedHash.length];generator.generateBytes(password.toCharArray(),computedHash);// 时间安全比较防止时序攻击returnconstantTimeEquals(expectedHash,computedHash);}/** * 常量时间比较防止时序攻击. */privatestaticbooleanconstantTimeEquals(byte[]a,byte[]b){if(a.length!b.length)returnfalse;intresult0;for(inti0;ia.length;i){result|a[i]^b[i];// 异或相同为0不同为非0}returnresult0;// 全部相同才返回true}}3.7 Spring Security 中使用 Argon2importorg.springframework.security.crypto.argon2.Argon2PasswordEncoder;// Spring Security 5.3 内置支持Argon2PasswordEncoderencodernewArgon2PasswordEncoder(16,// 盐长度32,// 哈希长度4,// 并行度65536,// 内存(KB)3// 迭代次数);Stringhashencoder.encode(myPassword123);booleanvalidencoder.matches(myPassword123,hash);3.8 Argon2 输出格式$argon2id$v19$m65536,t3,p4$c2FsdHNhbHQ$WQ1kF38L7MvA9vZ8pXtGw │ │ │ │ │ │ │ │ │ └─ 哈希值Base64无填充 │ │ │ └─ 盐值Base64无填充 │ │ └─ 参数m内存KB, t迭代次数, p并行度 │ └─ 版本号19 0x13 └─ 算法标识四、bcrypt vs Argon2 对比维度bcryptArgon2id发布年份19992015防 CPU 暴力✅ 可调轮数✅ 可调迭代次数防 GPU 并行⚠️ 内存需求小(4KB)GPU可大量并行✅ 内存需求大GPU并行受限防 ASIC⚠️ 已有专用芯片✅ 大内存让ASIC成本极高最大输入72字节截断无限制参数灵活性只有代价因子一个参数内存时间并行度三个参数生态成熟度极成熟所有语言/框架支持较新主流框架已支持推荐仍然安全可用新项目首选五、时序攻击与常量时间比较5.1 什么是时序攻击普通字符串比较遇到第一个不同字符就返回 false// 危险的比较方式publicbooleanunsafeEquals(Stringa,Stringb){if(a.length()!b.length())returnfalse;for(inti0;ia.length();i){if(a.charAt(i)!b.charAt(i))returnfalse;// 第一个不同就返回}returntrue;}攻击者可以测量响应时间输入 Axxxxxxx → 1字符匹配后返回 → 耗时 1μs 输入 Cxxxxxxx → 0字符匹配后返回 → 耗时 0.8μs → 第一个字符更可能是 A 或 B耗时更长说明匹配更多 逐位猜测就能推断出正确的哈希值5.2 常量时间比较// 安全的比较方式不管哪个位置不同总是比较完所有字节publicstaticbooleanconstantTimeEquals(byte[]a,byte[]b){if(a.length!b.length)returnfalse;intresult0;for(inti0;ia.length;i){result|a[i]^b[i];// 不提前返回始终遍历完}returnresult0;}bcrypt 和 Argon2 的验证实现内部都使用常量时间比较。六、实际应用中的密码存储方案6.1 Spring Boot 项目推荐配置importorg.springframework.context.annotation.Bean;importorg.springframework.security.crypto.factory.PasswordEncoderFactories;importorg.springframework.security.crypto.password.PasswordEncoder;ConfigurationpublicclassSecurityConfig{BeanpublicPasswordEncoderpasswordEncoder(){// 使用委托模式默认bcrypt支持多种算法// 输出格式{bcrypt}$2a$10$...// 旧数据可以标记为 {MD5}xxx 或 {sha256}xxx 兼容迁移returnPasswordEncoderFactories.createDelegatingPasswordEncoder();}}ServicepublicclassUserService{ResourceprivatePasswordEncoderpasswordEncoder;/** * 注册. */publicvoidregister(Stringusername,StringrawPassword){StringencodedPasswordpasswordEncoder.encode(rawPassword);// 存入数据库{bcrypt}$2a$10$N9qo8uLO...userRepository.save(newUser(username,encodedPassword));}/** * 登录验证. */publicbooleanlogin(Stringusername,StringrawPassword){UseruseruserRepository.findByUsername(username);if(usernull)returnfalse;returnpasswordEncoder.matches(rawPassword,user.getPassword());}}6.2 从 MD5 迁移到 bcrypt 的策略旧系统用 MD5 存密码如何平滑迁移/** * 双重验证 渐进迁移. */publicbooleanloginAndMigrate(Stringusername,StringrawPassword){UseruseruserRepository.findByUsername(username);StringstoredPassworduser.getPassword();if(storedPassword.startsWith({bcrypt})){// 新格式直接bcrypt验证returnpasswordEncoder.matches(rawPassword,storedPassword);}else{// 旧格式MD5验证Stringmd5HashDigestUtils.md5Hex(rawPassword);if(md5Hash.equals(storedPassword)){// 验证通过后用bcrypt重新哈希并更新StringnewHashpasswordEncoder.encode(rawPassword);user.setPassword(newHash);userRepository.save(user);returntrue;}returnfalse;}}七、选型决策新项目密码存储 │ ├── Java/Spring Boot 项目 │ → BCryptPasswordEncoder成熟稳定 │ → 或 Argon2PasswordEncoder更安全 │ ├── 安全要求极高金融/支付 │ → Argon2idm64MB, t3, p4 │ ├── 资源受限嵌入式/低配服务器 │ → bcryptcost10 │ └── 需要兼容旧系统 → DelegatingPasswordEncoder支持多算法并存一句话总结bcrypt 和 Argon2 的核心价值就是故意让计算变慢消耗资源从而让暴力破解变得不可行。bcrypt 是久经考验的选择Argon2 是面向未来硬件威胁的升级方案。