[{"data":1,"prerenderedAt":180},["ShallowReactive",2],{"article-hash-pbkdf2-salting-sha256-password-security":3},{"article":4,"relatedArticles":130},{"slug":5,"category":6,"date":7,"readingMinutes":8,"tags":9,"relatedTool":15,"cn":16,"en":65,"author":114,"reviewedBy":117,"reviewedDate":119,"updatedDate":119,"sources":120,"noIndex":129},"hash-pbkdf2-salting-sha256-password-security","最佳实践","2026-07-28",8,[10,11,12,13,14],"Salt","PBKDF2","SHA-256","Password Security","Cryptography","/tools/dev/hash",{"title":17,"description":18,"intro":19,"sections":20},"加盐 (Salt) 与 PBKDF2：为什么单纯用 SHA-256 存储用户密码是不安全的","解析彩虹表与 GPU 暴力破解密码机制，说明加盐与 PBKDF2 (HMAC-SHA256) 迭代慢哈希原理，并给出 Web Crypto API 密码哈希与验证代码。","仅使用单次 SHA-256 存储用户密码极易遭受彩虹表查表与 GPU 高速暴力破解。防范密码泄露的关键在于使用随机“盐”(Salt) 消除预计算查表，并利用 PBKDF2 等慢哈希算法增加单次计算耗时。本文深入分析加盐 PBKDF2 原理并提供 Web Crypto 实现。",[21,25,30,34,38,43,47,51,55,61],{"heading":22,"paragraphs":23},"一、问题概述：单次 SHA-256 存储密码的安全缺陷",[24],"许多开发者误以为“只要用了 SHA-256 这种不可逆哈希，密码就是安全的”。然而 SHA-256 是为高速吞吐量设计的消息摘要算法，现代 GPU 每秒可计算上百亿次 SHA-256 哈希。如果攻击者拿到数据库中未加盐的 SHA-256 散列值，可以通过彩虹表或暴力破解迅速还原出常见明文密码。",{"heading":26,"paragraphs":27,"code":29},"二、最小复现：未经加盐的明文密码被彩虹表匹配",[28],"下面的对比展示了单次 SHA-256 与加盐 PBKDF2 在面对预计算攻击时的安全差异：","/* 不安全做法：单次 SHA-256 (无 Salt) */\n// SHA256(\"123456\") = \"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92\"\n// 攻击者直接在公开彩虹表中查询该 Hex 即可秒级还原出明文 \"123456\"\n\n/* 安全做法：加盐 (Salt) + PBKDF2 迭代 600,000 次 */\n// 每位用户随机生成 16 字节 Salt，将单次破解耗时从纳秒级提高到毫秒级，使 GPU 批量破解成本飙升百万倍！",{"heading":31,"paragraphs":32},"三、根因分析：彩虹表查表攻击与 GPU 硬件算力压制",[33],"1. **彩虹表 (Rainbow Tables)**：利用“空间换时间”预先计算亿万常见密码的单次 SHA-256 哈希值。未加盐的密码在不同用户间相同，导致一份彩虹表能批量破译整个数据库。2. **GPU 算力碾压**：SHA-256 算法逻辑极其简单，定制 ASIC 或 GPU 密集阵列能并行极速计算。3. **慢哈希 (Key Derivation Function)**：PBKDF2 / Argon2 / bcrypt 通过成千上万次循环迭代（如 OWASP 推荐 PBKDF2-HMAC-SHA256 至少 600,000 次），人为拉长计算耗时。",{"heading":35,"paragraphs":36},"四、推荐方案：OWASP 密码安全标准与动态工作因子",[37],"1. 每用户独立 Salt：为每位用户生成至少 16 字节的密码学随机值。2. 算法选择：优先 Argon2id；不可用时考虑 scrypt；需要 FIPS-140 兼容实现时可使用 PBKDF2-HMAC-SHA256。bcrypt 主要用于遗留系统。3. 动态工作因子：当前 OWASP 对 PBKDF2-HMAC-SHA256 的基线是至少 600,000 次，但仍应在目标服务器上校准并随硬件提升升级。4. 数据库存储版本化参数：至少保存算法、Salt、工作因子与 Hash，便于日后迁移。",{"heading":39,"paragraphs":40,"code":42},"五、完整代码：基于 Web Crypto API 的 PBKDF2 密码派生与验证",[41],"下面的 TypeScript 代码同时实现创建记录和验证记录。示例 API 也可用于支持 Web Crypto 的服务端运行时；真正的密码存储与校验应放在服务端，并按服务器性能校准迭代次数。","interface PasswordHashResult {\n  saltHex: string;\n  hashHex: string;\n  iterations: number;\n}\n\nconst DEFAULT_PBKDF2_ITERATIONS = 600_000;\n\nfunction bytesToHex(bytes: Uint8Array): string {\n  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n  if (!/^(?:[0-9a-f]{2})+$/i.test(hex)) throw new Error(\"无效的十六进制数据\");\n  return Uint8Array.from(hex.match(/.{2}/g)!, (pair) => Number.parseInt(pair, 16));\n}\n\nasync function derivePassword(\n  password: string,\n  salt: Uint8Array,\n  iterations: number,\n): Promise\u003CUint8Array> {\n  const keyMaterial = await crypto.subtle.importKey(\n    \"raw\",\n    new TextEncoder().encode(password),\n    \"PBKDF2\",\n    false,\n    [\"deriveBits\"]\n  );\n  const bits = await crypto.subtle.deriveBits(\n    {\n      name: \"PBKDF2\",\n      salt,\n      iterations,\n      hash: \"SHA-256\",\n    },\n    keyMaterial,\n    256\n  );\n  return new Uint8Array(bits);\n}\n\nasync function hashPasswordPBKDF2(\n  password: string,\n  iterations = DEFAULT_PBKDF2_ITERATIONS,\n): Promise\u003CPasswordHashResult> {\n  const salt = crypto.getRandomValues(new Uint8Array(16));\n  const hash = await derivePassword(password, salt, iterations);\n  return {\n    saltHex: bytesToHex(salt),\n    hashHex: bytesToHex(hash),\n    iterations,\n  };\n}\n\nasync function verifyPasswordPBKDF2(\n  password: string,\n  stored: PasswordHashResult,\n): Promise\u003Cboolean> {\n  const expected = hexToBytes(stored.hashHex);\n  const actual = await derivePassword(password, hexToBytes(stored.saltHex), stored.iterations);\n  if (actual.length !== expected.length) return false;\n\n  // 不提前返回，避免把第一个不同字节的位置暴露为明显的时序差异。\n  let difference = 0;\n  for (let i = 0; i \u003C actual.length; i++) difference |= actual[i] ^ expected[i];\n  return difference === 0;\n}\n\nconst stored = await hashPasswordPBKDF2(\"MySecurePass123!\");\nconsole.log(await verifyPasswordPBKDF2(\"MySecurePass123!\", stored)); // true",{"heading":44,"paragraphs":45},"六、常见错误方案",[46],"在前端将密码加盐哈希后发送给后端（依然等同于硬编码明文 Password-as-a-Hash 凭据）；全局所有用户使用同一个静态硬编码 Salt；将 PBKDF2 迭代次数设置为 1。",{"heading":48,"paragraphs":49},"七、边界条件：前端加密 vs 服务端验证边界",[50],"密码加盐和 PBKDF2 计算应当在**服务端**完成。如果在前端完成 PBKDF2 并将 Hash 发送给服务端校验，攻击者只需窃取该 Hash 即可作为凭据直接登录（Pass-the-Hash 攻击）。前端只需使用 HTTPS 传输明文密码。",{"heading":52,"paragraphs":53},"八、如何验证密码哈希的防暴力破解能力",[54],"在目标服务器硬件上测量注册与登录的派生耗时，并在安全预算内校准迭代次数；单元测试应覆盖正确密码、错误密码、损坏记录，并断言相同密码配合不同 Salt 会产生不同 Hash。浏览器示例中的比较只能尽量避免提前返回；服务端应优先使用成熟密码库提供的常量时间比较。",{"heading":56,"paragraphs":57},"九、FAQ",[58,59,60],"问：PBKDF2、bcrypt 和 Argon2id 如何选择？答：一般优先 Argon2id；需要 FIPS-140 兼容实现时选择 PBKDF2-HMAC-SHA256；bcrypt 主要用于已有遗留系统。","问：只用 SHA-256 多重哈希（如循环 1000 次）可以吗？答：不应自创密码哈希方案；使用成熟、可配置并经过广泛审查的密码 KDF。","问：数据库泄露后加盐还有用吗？答：有用。每条记录的独立 Salt 让相同密码产生不同 Hash，阻止跨用户复用预计算结果，但弱密码仍可能被逐条离线猜测。",{"heading":62,"paragraphs":63},"十、总结",[64],"保护用户密码的物理核心是“阻慢攻击者”。使用高熵随机 Salt 抵御彩虹表，配合高迭代次数的 PBKDF2 / Argon2 抑制 GPU 暴破，才是现代化身份认证系统的标准防线。",{"title":66,"description":67,"intro":68,"sections":69},"Salting and PBKDF2: Why Plain Single-Iteration SHA-256 Password Hashing Is Unsafe","Examine rainbow table lookups and GPU brute-force mechanics, detailing random salt and PBKDF2 (HMAC-SHA256) slow hashing using the Web Crypto API.","Storing passwords using a single SHA-256 hash leaves them vulnerable to rainbow table lookups and high-speed GPU brute-force attacks. Password protection relies on unique per-user random salts to eliminate precomputed tables and PBKDF2 slow-hashing iterations. This article covers salted PBKDF2 mechanics and Web Crypto code.",[70,74,79,83,87,92,96,100,104,110],{"heading":71,"paragraphs":72},"1. Problem: security vulnerabilities of single-iteration SHA-256 password storage",[73],"Developers wrongly assume 'SHA-256 is an irreversible hash, making passwords safe.' However, SHA-256 is optimized for high-throughput message digestion; modern GPUs calculate tens of billions of SHA-256 hashes per second. Unsalted SHA-256 hashes in stolen databases are rapidly reversed via rainbow tables or brute force.",{"heading":75,"paragraphs":76,"code":78},"2. Minimal reproduction: unsalted plain passwords matched by rainbow tables",[77],"This comparison demonstrates the security gap between single SHA-256 and salted PBKDF2 against precomputation attacks:","/* Insecure: single-iteration SHA-256 (no salt) */\n// SHA256(\"123456\") = \"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92\"\n// Attackers look up this hex in a public rainbow table to recover \"123456\" in seconds!\n\n/* Secure: Random Salt + PBKDF2 (600,000 iterations) */\n// Generate a 16-byte random salt per user, increasing per-hash latency from nanoseconds to milliseconds.",{"heading":80,"paragraphs":81},"3. Root cause: rainbow table precomputation and GPU hardware dominance",[82],"1. **Rainbow tables**: precomputed lookup tables mapping billions of common passwords to SHA-256 hashes. Unsalted identical passwords produce identical hashes, enabling batch database cracking. 2. **GPU processing**: SHA-256's lightweight logic allows massive GPU parallelization. 3. **Key Derivation Functions (KDF)**: PBKDF2, Argon2, and bcrypt run thousands of iterations (e.g., OWASP recommends >= 600,000 iterations for PBKDF2-HMAC-SHA256) to intentionally slow down computation.",{"heading":84,"paragraphs":85},"4. Recommendation: OWASP password guidance and tunable work factors",[86],"1. Unique per-user salt: generate at least 16 cryptographically random bytes. 2. Algorithm choice: prefer Argon2id, use scrypt when it is unavailable, and use PBKDF2-HMAC-SHA256 when a FIPS-140-compatible implementation is required; bcrypt is mainly for legacy systems. 3. Work factor: OWASP currently lists at least 600,000 iterations for PBKDF2-HMAC-SHA256, but calibrate on target servers and upgrade over time. 4. Store versioned parameters: persist the algorithm, salt, work factor, and hash so records can be migrated.",{"heading":88,"paragraphs":89,"code":91},"5. Complete code: PBKDF2 password derivation and verification with Web Crypto",[90],"This TypeScript snippet implements both record creation and verification. The same API is available in Web Crypto-capable server runtimes; password storage and verification belong on the server, with iteration counts calibrated on production-class hardware.","interface PasswordHashResult {\n  saltHex: string;\n  hashHex: string;\n  iterations: number;\n}\n\nconst DEFAULT_PBKDF2_ITERATIONS = 600_000;\n\nfunction bytesToHex(bytes: Uint8Array): string {\n  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n  if (!/^(?:[0-9a-f]{2})+$/i.test(hex)) throw new Error(\"Invalid hexadecimal data\");\n  return Uint8Array.from(hex.match(/.{2}/g)!, (pair) => Number.parseInt(pair, 16));\n}\n\nasync function derivePassword(\n  password: string,\n  salt: Uint8Array,\n  iterations: number,\n): Promise\u003CUint8Array> {\n  const keyMaterial = await crypto.subtle.importKey(\n    \"raw\",\n    new TextEncoder().encode(password),\n    \"PBKDF2\",\n    false,\n    [\"deriveBits\"]\n  );\n  const bits = await crypto.subtle.deriveBits(\n    {\n      name: \"PBKDF2\",\n      salt,\n      iterations,\n      hash: \"SHA-256\",\n    },\n    keyMaterial,\n    256\n  );\n  return new Uint8Array(bits);\n}\n\nasync function hashPasswordPBKDF2(\n  password: string,\n  iterations = DEFAULT_PBKDF2_ITERATIONS,\n): Promise\u003CPasswordHashResult> {\n  const salt = crypto.getRandomValues(new Uint8Array(16));\n  const hash = await derivePassword(password, salt, iterations);\n  return {\n    saltHex: bytesToHex(salt),\n    hashHex: bytesToHex(hash),\n    iterations,\n  };\n}\n\nasync function verifyPasswordPBKDF2(\n  password: string,\n  stored: PasswordHashResult,\n): Promise\u003Cboolean> {\n  const expected = hexToBytes(stored.hashHex);\n  const actual = await derivePassword(password, hexToBytes(stored.saltHex), stored.iterations);\n  if (actual.length !== expected.length) return false;\n\n  let difference = 0;\n  for (let i = 0; i \u003C actual.length; i++) difference |= actual[i] ^ expected[i];\n  return difference === 0;\n}\n\nconst stored = await hashPasswordPBKDF2(\"MySecurePass123!\");\nconsole.log(await verifyPasswordPBKDF2(\"MySecurePass123!\", stored)); // true",{"heading":93,"paragraphs":94},"6. Incorrect approaches",[95],"Hashing passwords with PBKDF2 on the client before sending (acting as a fixed plaintext hash credential). Using a single static global salt for all users. Setting PBKDF2 iteration count to 1.",{"heading":97,"paragraphs":98},"7. Boundaries: client-side hashing vs. server-side verification",[99],"Password salting and PBKDF2 computation must occur on the **server**. Hashing client-side and sending the hash allows attackers to perform Pass-the-Hash authentication using the stolen hash directly. The client needs only HTTPS to transmit plaintext.",{"heading":101,"paragraphs":102},"8. Verification",[103],"Benchmark registration and login derivation on target server hardware and calibrate iterations within the security budget. Test correct passwords, wrong passwords, malformed records, and distinct salts. The JavaScript loop merely avoids an obvious early exit; on the server, prefer a mature password library with a constant-time comparison primitive.",{"heading":105,"paragraphs":106},"9. FAQ",[107,108,109],"How should I choose among PBKDF2, bcrypt, and Argon2id? Prefer Argon2id in general, use PBKDF2-HMAC-SHA256 when a FIPS-140-compatible implementation is required, and reserve bcrypt mainly for existing legacy systems.","Can I loop SHA-256 a thousand times instead? Do not invent a password hashing construction; use a mature, configurable, widely reviewed password KDF.","Does salting still help after a database leak? Yes. Unique salts prevent reuse of precomputed results across users, although weak passwords can still be guessed record by record.",{"heading":111,"paragraphs":112},"10. Summary",[113],"Protecting passwords relies on slowing down attackers. High-entropy random salts prevent rainbow tables, and multi-iteration PBKDF2/Argon2 neutralizes GPU brute-force capabilities.",{"name":115,"url":116},"Tiny's Tool Editorial Team","https://tinystool.org/about",{"name":118,"url":116},"Tiny's Tool Technical Review","2026-08-08",[121,125],{"title":122,"url":123,"publisher":124},"Password Storage Cheat Sheet","https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html","OWASP",{"title":126,"url":127,"publisher":128},"Web Cryptography Level 2","https://www.w3.org/TR/WebCryptoAPI/","W3C",false,[131,148,163],{"slug":132,"noIndex":133,"category":134,"date":7,"updatedDate":119,"reviewedDate":119,"readingMinutes":135,"tags":136,"relatedTool":15,"cn":142,"en":145},"hash-md5-charset-utf8-gbk-mismatch",true,"踩坑避坑",6,[137,138,139,140,141],"MD5","Hash","Charset","UTF-8","GBK",{"title":143,"description":144},"为什么前后端算出来的 MD5 不一致？字符集编码 (UTF-8 vs GBK) 隐形坑","剖析哈希函数作用于底层字节流而非抽象字符的本质，分析 UTF-8 与 GBK 编码下相同字符串物理字节差异导致的 MD5 不一致问题与校验对齐方案。",{"title":146,"description":147},"Why Front-End and Back-End MD5 Hashes Differ: UTF-8 vs. GBK Encoding Mismatches","Explain how hash functions process raw byte streams rather than abstract characters, detailing UTF-8 vs. GBK encoding mismatches and resolution strategies.",{"slug":149,"noIndex":129,"category":150,"date":7,"updatedDate":119,"reviewedDate":119,"readingMinutes":151,"tags":152,"relatedTool":15,"cn":157,"en":160},"hash-web-crypto-api-large-file","实现原理",5,[153,154,155,156],"Web Crypto API","Large File Hash","Async","Performance",{"title":158,"description":159},"Web Crypto 计算文件 SHA-256：非流式 digest 的内存边界与大文件方案","澄清 Web Crypto API 不支持流式 digest：中小文件可设置上限后一次性计算，真正的大文件应在 Worker 中使用可信的增量 SHA-256 实现。",{"title":161,"description":162},"File SHA-256 with Web Crypto: Memory Bounds and a Real Large-File Strategy","Explain that Web Crypto digest is non-streaming: use it only below an explicit file limit, and use a vetted incremental SHA-256 implementation in a Worker for genuinely large files.",{"slug":164,"noIndex":133,"category":6,"date":165,"updatedDate":165,"reviewedDate":119,"readingMinutes":166,"tags":167,"relatedTool":173,"cn":174,"en":177},"git-complete-tutorial","2026-07-29",7,[168,169,170,171,172],"Git","Version Control","DevOps","Workflow","Tutorial","/articles/software",{"title":175,"description":176},"Git 使用全教程：从工作区原理、现代分支命令到救命灾难恢复指南","全面讲解 Git 三大区域划分、现代 `git switch/restore` 命令、分支管理策略、Merge 与 Rebase 异同、冲突解决以及基于 `git reflog` 的恢复救命手册。",{"title":178,"description":179},"Git Complete Guide: From Core Architecture to Modern Commands and Reflog Recovery","Master Git 3-area architecture, modern `git switch/restore` commands, branching strategies, Merge vs Rebase trade-offs, conflict resolution, and `git reflog` recovery.",1786725508319]