ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

密码哈希算法 — bcrypt 与 Argon2 详解

2026/8/13 2:39:02 拓冰建站 浏览量
密码哈希算法 — bcrypt 与 Argon2 详解

密码哈希算法 — 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

二、bcrypt

2.1 什么是 bcrypt

基于 Blowfish 加密算法改造的密码哈希函数。1999年发布,至今仍是最广泛使用的密码哈希方案。

2.2 核心特性

特性说明
自适应代价因子可以调整计算轮数,硬件变快就增加轮数
内置盐值自动生成随机盐,不需要手动管理
固定输出格式输出包含算法标识+代价因子+盐+哈希值
最大输入限制只处理前72字节(超长密码截断)

2.3 输出格式解析

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy │ │ │ │ │ │ │ │ │ │ │ └─ 哈希值(31字符) │ │ │ │ └─ 盐值(22字符,Base64编码的16字节) │ │ │ └─ 代价因子(10表示2^10=1024轮) │ │ └─ 分隔符 │ └─ 算法版本(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{// 创建编码器,参数是代价因子(默认10)privatestaticfinalBCryptPasswordEncoderencoder=newBCryptPasswordEncoder(12);/** * 注册时:对密码进行哈希. */publicstaticStringhashPassword(StringrawPassword){Stringhashed=encoder.encode(rawPassword);// 输出示例:$2a$12$LJ3m4sMRpNeU8WpDU8h7tuQOBCKFPBRjPfOcg1vKLAvEjxV3mGCHareturnhashed;}/** * 登录时:验证密码. */publicstaticbooleanverifyPassword(StringrawPassword,StringstoredHash){returnencoder.matches(rawPassword,storedHash);}publicstaticvoidmain(String[]args){Stringpassword="myPassword123";// 注册 — 生成哈希存入数据库Stringhash1=hashPassword(password);Stringhash2=hashPassword(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 次,每次都是完整的加密运算 为什么不能并行加速? 每次循环依赖上一次的结果(串行依赖)

三、Argon2

3.1 什么是 Argon2

2015年 Password Hashing Competition 的冠军算法。设计目标:抵抗 GPU/FPGA/ASIC 等专用硬件的暴力破解。

3.2 bcrypt 的不足

bcrypt 只消耗 CPU 时间,而现代攻击者有大量 GPU:

CPU:串行计算,一个核心一次算一个 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 KB(64MB)
时间代价(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{privatestaticfinalintMEMORY=65536;// 64MBprivatestaticfinalintITERATIONS=3;// 3轮privatestaticfinalintPARALLELISM=4;// 4线程privatestaticfinalintHASH_LENGTH=32;// 32字节输出privatestaticfinalintSALT_LENGTH=16;// 16字节盐/** * Argon2id 哈希密码. */publicstaticStringhashPassword(Stringpassword){// 生成随机盐byte[]salt=newbyte[SALT_LENGTH];newSecureRandom().nextBytes(salt);// 配置参数Argon2Parametersparams=newArgon2Parameters.Builder(Argon2Parameters.ARGON2_id).withMemoryAsKB(MEMORY).withIterations(ITERATIONS).withParallelism(PARALLELISM).withSalt(salt).build();// 计算哈希Argon2BytesGeneratorgenerator=newArgon2BytesGenerator();generator.init(params);byte[]hash=newbyte[HASH_LENGTH];generator.generateBytes(password.toCharArray(),hash);// 编码为存储格式:$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>StringsaltBase64=Base64.getEncoder().withoutPadding().encodeToString(salt);StringhashBase64=Base64.getEncoder().withoutPadding().encodeToString(hash);returnString.format("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",MEMORY,ITERATIONS,PARALLELISM,saltBase64,hashBase64);}/** * 验证密码. */publicstaticbooleanverifyPassword(Stringpassword,StringstoredHash){// 解析存储的哈希值中的参数和盐String[]parts=storedHash.split("\\$");// parts[3] = "m=65536,t=3,p=4"// parts[4] = salt (Base64)// parts[5] = hash (Base64)String[]paramParts=parts[3].split(",");intmemory=Integer.parseInt(paramParts[0].substring(2));intiterations=Integer.parseInt(paramParts[1].substring(2));intparallelism=Integer.parseInt(paramParts[2].substring(2));byte[]salt=Base64.getDecoder().decode(parts[4]);byte[]expectedHash=Base64.getDecoder().decode(parts[5]);// 用相同参数重新计算Argon2Parametersparams=newArgon2Parameters.Builder(Argon2Parameters.ARGON2_id).withMemoryAsKB(memory).withIterations(iterations).withParallelism(parallelism).withSalt(salt).build();Argon2BytesGeneratorgenerator=newArgon2BytesGenerator();generator.init(params);byte[]computedHash=newbyte[expectedHash.length];generator.generateBytes(password.toCharArray(),computedHash);// 时间安全比较(防止时序攻击)returnconstantTimeEquals(expectedHash,computedHash);}/** * 常量时间比较(防止时序攻击). */privatestaticbooleanconstantTimeEquals(byte[]a,byte[]b){if(a.length!=b.length)returnfalse;intresult=0;for(inti=0;i<a.length;i++){result|=a[i]^b[i];// 异或:相同为0,不同为非0}returnresult==0;// 全部相同才返回true}}

3.7 Spring Security 中使用 Argon2

importorg.springframework.security.crypto.argon2.Argon2PasswordEncoder;// Spring Security 5.3+ 内置支持Argon2PasswordEncoderencoder=newArgon2PasswordEncoder(16,// 盐长度32,// 哈希长度4,// 并行度65536,// 内存(KB)3// 迭代次数);Stringhash=encoder.encode("myPassword123");booleanvalid=encoder.matches("myPassword123",hash);

3.8 Argon2 输出格式

$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHQ$WQ1kF3+8L7MvA9vZ8pXtGw │ │ │ │ │ │ │ │ │ └─ 哈希值(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(inti=0;i<a.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;intresult=0;for(inti=0;i<a.length;i++){result|=a[i]^b[i];// 不提前返回,始终遍历完}returnresult==0;}

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){StringencodedPassword=passwordEncoder.encode(rawPassword);// 存入数据库:{bcrypt}$2a$10$N9qo8uLO...userRepository.save(newUser(username,encodedPassword));}/** * 登录验证. */publicbooleanlogin(Stringusername,StringrawPassword){Useruser=userRepository.findByUsername(username);if(user==null)returnfalse;returnpasswordEncoder.matches(rawPassword,user.getPassword());}}

6.2 从 MD5 迁移到 bcrypt 的策略

旧系统用 MD5 存密码,如何平滑迁移?

/** * 双重验证 + 渐进迁移. */publicbooleanloginAndMigrate(Stringusername,StringrawPassword){Useruser=userRepository.findByUsername(username);StringstoredPassword=user.getPassword();if(storedPassword.startsWith("{bcrypt}")){// 新格式:直接bcrypt验证returnpasswordEncoder.matches(rawPassword,storedPassword);}else{// 旧格式:MD5验证Stringmd5Hash=DigestUtils.md5Hex(rawPassword);if(md5Hash.equals(storedPassword)){// 验证通过后,用bcrypt重新哈希并更新StringnewHash=passwordEncoder.encode(rawPassword);user.setPassword(newHash);userRepository.save(user);returntrue;}returnfalse;}}

七、选型决策

新项目密码存储? │ ├── Java/Spring Boot 项目 │ → BCryptPasswordEncoder(成熟稳定) │ → 或 Argon2PasswordEncoder(更安全) │ ├── 安全要求极高(金融/支付) │ → Argon2id(m=64MB, t=3, p=4) │ ├── 资源受限(嵌入式/低配服务器) │ → bcrypt(cost=10) │ └── 需要兼容旧系统 → DelegatingPasswordEncoder(支持多算法并存)

一句话总结:bcrypt 和 Argon2 的核心价值就是"故意让计算变慢+消耗资源",从而让暴力破解变得不可行。bcrypt 是久经考验的选择,Argon2 是面向未来硬件威胁的升级方案。