很多后端同学在做账号体系的时候,都会遇到同一个问题:密码到底能不能直接做一次 MD5 再存数据库?网上说法很多,有的说 MD5 不安全,有的说加盐之后就可以,还有的提到了 BCrypt、PBKDF2、Argon2 这些名词。如果你是从热搜词salt或者salt player open进到这篇文章,先说清楚一个概念:这里讨论的并不是某个叫 Salt 的开源播放器,而是密码学中非常核心的“加盐(Salt)”机制。在项目评审里,“开启加盐”经常会被翻译成五花八门的中文,本质上都是要求密码不能裸哈希存储。
这篇文章我会围绕密码加盐展开,把盐是什么、盐解决什么问题、如何用 Java 原生 API 实现一套可落地的加盐哈希方案、以及 Spring Security 中 BCrypt 的自动加盐机制都梳理一遍。内容偏实战,代码可以直接复制到本地项目里运行,最后还会整理高频报错和工程建议,适合正在做账号系统、登录模块、安全改造的后端开发者。
1. 为什么密码不能只做哈希存储
1.1 哈希算法是单向的,但不是为密码设计
很多人刚接触安全时,会以为“MD5 是不可逆的,所以密码存 MD5 就是安全的”。这个理解存在两个偏差。
第一,哈希算法确实是单向函数,从哈希值反推原始输入在计算上非常困难。但 MD5、SHA-1、SHA-256 这类通用哈希算法在设计目标上追求的是“高效”,它们运行速度非常快。对于开发者来说,快是好事;对于攻击者来说,快同样是好事。攻击者可以在一秒内尝试数十亿次密码猜测,直接把常见密码字典里的每一个词都做一次哈希,然后和你数据库里的哈希值比对。
第二,“不可逆”只表示不能从哈希值直接还原出明文,但攻击者根本不需要还原。他们只需要用常见密码库去碰撞。如果你的用户密码是123456,那几乎任何一本预计算字典里都有它的 MD5 值。
1.2 相同密码产生相同哈希值导致的问题
通用哈希算法是确定性的:同样的输入,永远得到同样的输出。这带来一个非常严重的问题,如果两个用户都设置了admin123,那么他们的密码哈希值完全一样。
攻击者一旦破解出其中一个人的明文,就等于同时知道了所有相同哈希值对应的明文。即便只是从统计角度,他也可以判断出哪些用户使用了相同密码,这给撞库攻击提供了很大便利。
更经典的风险是彩虹表。彩虹表是一种用空间换时间的预计算结果集合,攻击者提前把海量密码的哈希算好并建立索引。在彩虹表面前,没有加盐的 MD5、SHA-1 基本等同于明文。虽然彩虹表不能覆盖所有密码,但覆盖几亿条常见弱密码是没有任何压力的。
1.3 加盐之后效果完全不同
给密码加盐,就是在用户输入的原始密码后面拼接一段随机数据,然后再做哈希。
MD5(password) MD5(password + salt)这两者最大的区别在于,前者只要密码相同,哈希值就固定;后者因为每次注册都会生成一段新的随机盐,所以即使两个用户使用同一个密码,最终的哈希值也完全不同。
盐的作用,本质上是把“指定密码的哈希”变成“指定密码在指定盐下的哈希”。攻击者如果要破解,就无法复用预先算好的彩虹表,只能针对每个盐值单独做暴力破解。这样一来,破解成本被大幅提高。
2. 密码加盐的核心概念
2.1 盐的本质
盐是一串随机数据,通常以字节数组的形式存在,再通过 Base64 编码存储。它本身并不是秘密,甚至可以和最终的密码哈希值存放在同一个数据库字段里。这听起来可能有些反直觉,很多人会问:“盐如果不保密,加了有什么用?”
盐并不是靠“保密”来生效的,它的核心价值是“随机性”。攻击者可以拿到你的盐,但他依然需要针对这个随机盐重新计算字典里的每一个候选密码。假设你的盐有 128 位随机熵,攻击者想要预计算一张覆盖所有可能盐和密码组合的彩虹表,这在计算上是不可能完成的。
2.2 盐的关键属性
第一,盐必须足够随机。这里的“随机”指的是密码学意义上的随机,要使用SecureRandom这类加密安全伪随机数生成器,而不是Math.random()。Math.random()的随机性和安全性都不够强,在密码安全场景下不能使用。
第二,盐的长度不能太短。一般建议 16 字节,也就是 128 位。太短的话,攻击者仍然可能通过大量预计算覆盖常见组合。
第三,每个用户每次注册都必须使用新的盐,不能全项目共用一个固定盐。如果使用固定盐,虽然可以防止彩虹表攻击,但两个相同密码的用户仍然会得到相同哈希,而且一旦攻击者分析出那个固定盐,整个项目的密码体系就全部暴露了。
第四,盐要和密码哈希一起存储。这一点很重要。很多同学会把加盐机制想复杂,认为盐也要单独加密保存。其实不需要,只要盐的随机性足够,它完全可以以明文方式放在存储字符串中。
2.3 盐与胡椒的区别
在安全资料中,你可能会看到两个相近的概念:Salt(盐)和 Pepper(胡椒)。
盐是随机的、不保密的、每个用户不同的数据。胡椒是一个全局的、保密的、可以作为应用级密钥的数据。例如:
hash = H(password + salt + pepper)胡椒不能和密码哈希存在同一个数据库里,通常放在独立的配置中心、环境变量或者密钥管理系统中。一旦 pepper 泄露,它的保密价值也就消失了。
实际开发中,加盐通常是必选项,而胡椒属于额外加固手段。如果是刚开始做安全改造,先保证盐的实现正确,再考虑引入胡椒。
3. 环境准备与版本说明
本文的实战示例以 Java 语言为主,具体环境如下:
- 操作系统:Windows / Linux / macOS 均可
- JDK:JDK 8 以上即可运行,示例代码默认以 JDK 17 演示
- 构建工具:Maven 3.6+ 或直接使用 IDE 内置依赖管理
- IDE:IntelliJ IDEA 或 Eclipse
- 框架:第四章使用 JDK 原生 API,不依赖第三方库;第五章使用 Spring Boot 3.x + Spring Security 6.x
如果你本地的 JDK 版本不同,不需要担心。第四章里用到的SecureRandom、PBEKeySpec、SecretKeyFactory都是 JDK 自带类,从 Java 8 开始就稳定存在。版本不同的主要影响是安全策略和算法支持细节,核心代码不用改动。
4. 使用 Java 原生 API 实现密码加盐
这一章我们会实现一个完整的密码加盐、哈希、编码、校验工具类。整个过程不使用任何第三方库,方便你先理解底层原理。
4.1 项目结构与依赖
先创建一个普通的 Maven 项目,结构如下:
password-salt-demo ├── pom.xml └── src └── main └── java └── com └── example └── password ├── PasswordUtil.java └── PasswordDemo.java这里不需要在pom.xml中额外引入依赖,因为我们会使用 JDK 自带的密码学 API。下面是可以直接使用的pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>password-salt-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </project>4.2 生成随机盐
生成盐的核心是使用SecureRandom。这个类提供了加密安全的随机数生成能力,系统会尽可能从操作系统底层随机源获取熵。
package com.example.password; import java.security.SecureRandom; import java.util.Base64; public final class PasswordSaltGenerator { private static final SecureRandom SECURE_RANDOM = new SecureRandom(); private PasswordSaltGenerator() { } public static String generateSalt(int byteLength) { byte[] salt = new byte[byteLength]; SECURE_RANDOM.nextBytes(salt); return Base64.getEncoder().encodeToString(salt); } public static void main(String[] args) { String salt1 = generateSalt(16); String salt2 = generateSalt(16); System.out.println("第一次生成盐: " + salt1); System.out.println("第二次生成盐: " + salt2); System.out.println("两次盐是否相同: " + salt1.equals(salt2)); } }运行后,你会发现每次生成的盐都不相同。这里把SecureRandom声明为静态常量,是推荐的做法。频繁创建SecureRandom实例可能会出现性能问题,而复用同一个实例既可以保证随机源质量,也能减少开销。
注意,byteLength参数代表的是随机字节数。16 字节等于 128 位,这一步在生产环境中至少要保证达到该长度。
4.3 密码哈希与编码格式
有了盐之后,下一步就是使用 PBKDF2 算法计算密码哈希。PBKDF2 的全称是 Password-Based Key Derivation Function 2,它通过反复执行伪随机函数来增加暴力破解的成本。Java 中可以使用SecretKeyFactory配合PBEKeySpec来实现。
我们将完整定义密码的存储格式。推荐使用类似下面的结构:
pbkdf2_sha256$迭代次数$盐$哈希值使用$作为分隔符,好处是后续解析时非常容易,也方便未来切换算法。例如从 PBKDF2 切换到 BCrypt 时,可以通过前缀区分。
下面是完整的PasswordUtil代码:
package com.example.password; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Base64; public final class PasswordUtil { private static final int SALT_BYTE_SIZE = 16; private static final int ITERATIONS = 100_000; private static final int HASH_BIT_SIZE = 256; private static final String ALGORITHM = "PBKDF2WithHmacSHA256"; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); private PasswordUtil() { } /** * 生成随机盐 */ public static String generateSalt() { byte[] salt = new byte[SALT_BYTE_SIZE]; SECURE_RANDOM.nextBytes(salt); return Base64.getEncoder().encodeToString(salt); } /** * 生成加密后的密码字符串 */ public static String encode(String password) throws Exception { String salt = generateSalt(); String hash = pbkdf2(password, salt, ITERATIONS, HASH_BIT_SIZE); return "pbkdf2_sha256$" + ITERATIONS + "$" + salt + "$" + hash; } /** * 校验密码 */ public static boolean verify(String password, String encoded) throws Exception { String[] parts = encoded.split("\\$"); if (parts.length != 4) { throw new IllegalArgumentException("密码存储格式不正确"); } String algorithm = parts[0]; int iterations = Integer.parseInt(parts[1]); String salt = parts[2]; String expectedHash = parts[3]; if (!"pbkdf2_sha256".equals(algorithm)) { throw new IllegalArgumentException("不支持的算法: " + algorithm); } String actualHash = pbkdf2(password, salt, iterations, HASH_BIT_SIZE); return MessageDigest.isEqual( Base64.getDecoder().decode(actualHash), Base64.getDecoder().decode(expectedHash) ); } private static String pbkdf2(String password, String salt, int iterations, int keyLengthBits) throws Exception { byte[] saltBytes = Base64.getDecoder().decode(salt); PBEKeySpec spec = new PBEKeySpec( password.toCharArray(), saltBytes, iterations, keyLengthBits ); SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); byte[] hashBytes = factory.generateSecret(spec).getEncoded(); return Base64.getEncoder().encodeToString(hashBytes); } }这段代码中的几个关键点需要说明。
PBEKeySpec的第一个参数使用char[]而不是String,这是安全编码的常见实践。字符串是不可变对象,会一直存在于内存中,而字符数组用完之后可以手动清空。虽然示例中没有显式清空,但你已经可以看到char[]的用法。
ITERATIONS是迭代次数,默认设置为 100000。迭代次数直接影响两个指标:一方面是哈希计算速度,另一方面是攻击者暴力破解的成本。次数越大,破解成本越高,但正常用户登录时的耗时也会增加。这里的 100000 适合作为入门示例,生产环境需要根据服务器性能和最新安全建议调整,后面最佳实践部分会再细说。
比较哈希值时,使用了MessageDigest.isEqual,而不是直接调用equals。MessageDigest.isEqual会使用常量时间比较,避免通过时间差异泄露信息,能够减少时序攻击风险。
4.4 密码校验
密码校验是登录功能的核心。校验流程并不复杂,从存储字符串中拆出盐和迭代次数,再用用户输入的明文密码重新计算哈希,最后对比两个哈希值是否相等。
这里必须强调一个容易出错的地方:校验时不要重新生成新盐。盐是随机的,重新生成会导致哈希永远对不上。校验使用的盐必须来自数据库里存储的那条记录。
下面是完整的演示类:
package com.example.password; public class PasswordDemo { public static void main(String[] args) throws Exception { String rawPassword = "csdn-demo-123"; String encoded1 = PasswordUtil.encode(rawPassword); String encoded2 = PasswordUtil.encode(rawPassword); System.out.println("第一次加密结果: " + encoded1); System.out.println("第二次加密结果: " + encoded2); System.out.println("两次结果是否相同: " + encoded1.equals(encoded2)); System.out.println(); System.out.println("正确密码校验: " + PasswordUtil.verify(rawPassword, encoded1)); System.out.println("错误密码校验: " + PasswordUtil.verify("wrong-pass", encoded1)); } }4.5 运行与验证
运行main方法后,你大概率会看到类似下面的输出:
第一次加密结果: pbkdf2_sha256$100000$T9m8w6Xa2Ir1HdJkYwF1eA==$9sZg1Vv... 第二次加密结果: pbkdf2_sha256$100000$QmFzZTY0U2FsdA==$4Fq2Xv... 两次结果是否相同: false 正确密码校验: true 错误密码校验: false输出结果说明了两件事。第一,同一个原始密码,经过盐处理后得到的是两个完全不同的编码字符串。第二,校验逻辑可以正确区分正确密码和错误密码。
这个工具类已经具备在生产代码中使用的雏形。你可以把encode的返回值存到数据库的password字段,登录时取出该字段,再调用verify完成校验。整个过程不依赖任何后端框架,适合理解原理,也适合被封装到公共工具库中。
5. 用 Spring Security BCrypt 自动处理盐
理解了手写加盐过程之后,再来看 Spring Security 中常见的 BCrypt 方案,思路就清晰很多。BCrypt 是一种自带盐的密码哈希算法,它不需要像 PBKDF2 那样手动生成盐并单独存储。
5.1 BCrypt 为什么更推荐
BCrypt 的优势主要有三点。
第一,BCrypt 自动包含随机盐。每个密码经过 BCrypt 处理后,输出的字符串本身就带有盐和成本因子,开发者不需要关心盐的生成、拼接和存储。
第二,BCrypt 被设计为计算密集型算法。它会执行多次 Blowfish 密钥扩展,天然比 MD5、SHA-256 慢很多。这个“慢”正是密码哈希想要的特性。
第三,BCrypt 支持成本因子调整。随着硬件性能提升,可以逐渐提高成本因子,增加攻击难度,而不需要改变存储格式。
5.2 引入依赖
如果你使用的是 Spring Boot 3.x,可以先创建标准项目,然后引入 Spring Security 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>引入该依赖后,Spring Boot 会自动启用默认安全配置。如果没有额外配置,访问任何接口都需要登录,并生成一个随机密码打印在启动日志里。这是符合预期的,我们再通过配置类显式定义PasswordEncoder。
5.3 配置 PasswordEncoder
新建配置类:
package com.example.security.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }这里的BCryptPasswordEncoder默认使用强度为 10 的成本因子。如果你希望提高强度,可以通过构造参数指定:
return new BCryptPasswordEncoder(12);成本因子越大,计算耗时越长。需要根据项目实际访问量和服务器性能权衡,建议压测后再调整,不要盲目调到特别大的数值。
5.4 注册和登录示例
在业务代码中,我们只依赖PasswordEncoder接口,避免直接耦合 BCrypt 实现。注册逻辑如下:
package com.example.security.service; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserService { private final PasswordEncoder passwordEncoder; public UserService(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public void register(String username, String rawPassword) { String encodedPassword = passwordEncoder.encode(rawPassword); System.out.println("需要写入数据库的密码: " + encodedPassword); // 执行 userMapper.insert(username, encodedPassword) } public boolean login(String username, String rawPassword) { // 从数据库查出该用户存储的密码哈希 String encodedPassword = queryEncodedPasswordByUsername(username); return passwordEncoder.matches(rawPassword, encodedPassword); } private String queryEncodedPasswordByUsername(String username) { // 实际项目中替换为数据库查询 return "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"; } }注册时调用passwordEncoder.encode(rawPassword),返回的字符串以$2a$开头,其中包含版本标识、成本因子、盐和哈希值。登录时调用passwordEncoder.matches(rawPassword, encodedPassword),方法内部会自动解析出盐和成本因子,再重新计算并比较。
这里有一个非常常见的误区:登录校验时,一定不要先对密码做encode再matches。matches的第一个参数必须是明文密码,第二个参数才是数据库中的哈希值。如果你的代码写成了matches(encode(rawPassword), encodedPassword),结果几乎必然是 false,而且会让 BCrypt 莫名其妙地多计算一次哈希。
5.5 BCrypt 校验原理简析
BCrypt 生成的字符串结构大致如下:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy其中$2a$是算法版本,10是成本因子,后面 22 个字符是盐,再往后是哈希值。因为它把盐和哈希放在了同一个字符串中,所以校验时不需要额外查询盐字段。这也是 BCrypt 相比“手动加盐 PBKDF2”更方便的原因。
如果登录接口报错或者密码始终对不上,可以先检查数据库里的密码字符串是否完整,很多情况下是因为字段长度不足,导致末尾部分被截断。数据库字段建议设置为VARCHAR(255),至少也要 100 个字符,否则存储会出问题。
6. 常见问题与排查思路
6.1 常见问题对照表
我在做账号安全改造时,收集过不少实际遇到的高频问题。下面用表格直接列出现象、原因和解决思路:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 两个相同密码的用户,数据库里的密码哈希完全一样 | 使用固定盐,或者完全没有加盐 | 每个用户注册时生成新的随机盐 |
使用Math.random()生成盐 | 随机性不够,存在安全风险 | 改用SecureRandom |
| 登录时密码校验总是失败 | 校验时重新生成了新的盐,或传入的密码已经被 encode 过 | 校验时使用数据库里的盐;matches第一个参数必须是明文 |
PBKDF2 算法报NoSuchAlgorithmException | JDK 版本过低或算法名称拼写错误 | 使用 JDK 8+;确认算法名称为PBKDF2WithHmacSHA256 |
| 数据库插入密码字段报错 | 字段长度不够,编码字符串被截断 | 将密码字段改为VARCHAR(255) |
| 登录接口耗时明显增加 | 迭代次数或 BCrypt 成本因子设置过高 | 结合服务器 CPU 性能压测,适当降低参数 |
数据库中哈希值以$2a$开头,却使用 PBKDF2 的verify去校验 | 新旧算法混用,没有根据前缀区分 | 解析存储字符串时先识别算法名,再走不同校验逻辑 |
| 直接对哈希值做解密 | 哈希是单向的,不能解密 | 用户忘记密码时重置,而不是找回原密码 |
6.2 一个典型误区的说明
有同学会在测试时发现,同一台机器上对同一个密码执行两次passwordEncoder.encode(),得到的字符串完全不同,于是怀疑代码有问题。这个现象其实是正常的,因为 BCrypt 每次都会生成随机盐。
判断不能靠“两次 encode 的结果是否一致”,而应该用matches方法对第一次的编码结果做校验。只要matches返回 true,说明整个加盐、哈希、存储、校验链路是通的。
如果你在代码评审中看到有同事断言“BCrypt 加盐后密码固定”,那说明他对盐的机制理解还有偏差。每次随机盐的价值就在于此:它让攻击者无法通过预计算表批量破解。
7. 最佳实践与工程建议
7.1 算法选择
密码哈希算法不是越新越好,而是越适合密码存储越好。目前主流的密码哈希算法包括 BCrypt、PBKDF2、scrypt、Argon2。不同语言和框架的支持情况不同,Java 生态中最常见的是 BCrypt 和 PBKDF2。
如果项目已经引入了 Spring Security,直接用BCryptPasswordEncoder是最省事的方案,安全强度有保障,而且实现经过了大量社区验证。如果项目不方便引入额外依赖,使用 JDK 自带的 PBKDF2 也是正确选择。重点不是纠结哪个算法天下第一,而是不要再使用单纯的 MD5、SHA-1、SHA-256 直接存密码。
7.2 盐的生成与管理
盐必须由密码学安全的伪随机数生成器产生。Java 中就是SecureRandom。在实现时应遵循以下原则:
- 每个用户每次注册都要新生成一个盐。
- 盐的字节长度至少 16。
- 盐不需要加密存储。
- 盐和哈希值建议拼接为一个格式字符串,降低字段管理复杂度。
- 不要在前端页面上暴露用户的盐或哈希值。
7.3 密码存储格式
统一存储格式非常重要。推荐使用算法名$迭代次数$盐$哈希这种带元信息的格式。这样未来算法升级时,系统可以根据前缀判断新旧哈希,实现平滑过渡。
举个例子,你现在的存量用户可能都是md5$xxxxxxxx,新用户是bcrypt$xxxxxxxx。登录校验时,代码先看前缀,如果是旧格式,就先把旧密码迁移成新格式,再写入数据库。这个“登录时渐进式迁移”的策略,比一次全量迁移要安全得多。
if (encodedPassword.startsWith("md5$")) { boolean ok = legacyMd5Verify(rawPassword, encodedPassword); if (ok) { String newEncoded = passwordEncoder.encode(rawPassword); updatePassword(username, newEncoded); } return ok; } return passwordEncoder.matches(rawPassword, encodedPassword);7.4 合规与审计
密码存储属于账号安全体系的一部分,上线前建议自查以下几点:
- 是否禁止了弱密码,比如长度低于 8 位、纯数字、常见密码列表。
- 是否对登录失败次数做了限制,防止暴力破解。
- 是否在日志中打印了密码、密码哈希或盐。日志中禁止出现任何凭证相关敏感信息。
- 数据库账号是否遵循最小权限原则,生产环境不要用高权限账号执行业务 SQL。
- 是否支持用户主动修改密码和注销会话。
7.5 生产环境改造建议
如果是存量系统,不建议直接全表重算密码。更稳妥的方式是先上线新算法,让新注册用户使用新格式,同时通过登录校验完成老用户渐进式迁移。整个过程中需要注意以下几个风险点:
- 修改密码字段长度前,先在测试环境验证所有 SQL 和索引。
- 如果使用 Spring Security,注意引入依赖后默认拦截所有接口,需要提前确认哪些接口需要放行。
- 如果使用自研
PasswordUtil,建议补充单元测试,覆盖正确密码、错误密码、空密码、超长密码等边界情况。 - 迭代次数和 BCrypt 成本因子不宜设置过高。压测时关注登录接口的 TP99 耗时,通常建议单次哈希耗时控制在 100ms 到 300ms 之间,具体根据业务场景权衡。
8. 总结与下一步学习建议
密码加盐是账号体系安全改造中绕不开的一环。通过本文,你应该已经理解了盐的本质、盐与哈希的关系、手动实现 PBKDF2 加盐的完整流程,以及 Spring Security BCrypt 如何自动管理盐。同时,你也知道了为什么不能直接使用 MD5 存密码,以及在登录校验中容易踩的坑。
如果接下来想继续深入,建议按以下顺序学习:
- 先阅读 Spring Security 官方文档中关于 Password Storage 的部分。
- 再对比研究 BCrypt、scrypt、Argon2 三种算法的设计目标和适用场景。
- 然后了解 WebAuthn 和无密码登录方案,看看是否能在自己的项目中引入更现代的身份认证方式。
- 最后结合项目实际情况,把密码重置、找回、登录限流、账号锁定等内容一起补全。
安全没有绝对的一劳永逸,每次硬件性能提升后,都需要重新评估哈希算法的成本参数。但无论技术怎么演进,加盐这个基础设计都不会过时。哪怕是做内部管理系统、毕业设计、个人博客项目,只要涉及账号密码,都应该把加盐这件事写进代码规范里。