用户密码直接以明文形式保存在数据库里,是老系统中最常见的安全隐患之一。只要数据库被拖库或者内部人员泄露数据,所有用户的密码就会暴露无遗。MD5 加盐虽然不是当下最前沿的方案,但它思路简单、落地成本低,非常适合作为小型项目或学习安全存储的入门实践。本文将以 Spring Boot 为例,完整演示从注册加密到登录校验的整个流程。

一、为什么单纯 MD5 不安全,加盐解决了什么问题
MD5 是一种单向哈希算法,输入任意长度的内容都会输出固定的 128 位摘要,理论上无法从摘要反推出原文。很多人因此认为把密码做一次 MD5 就安全了,这其实是一个常见误区。MD5 有两个致命弱点:一是相同输入永远得到相同输出,二是它运算速度极快。
第一个弱点意味着,如果两个用户设置了同一个密码,数据库里的密文就是一样的,攻击者很容易顺藤摸瓜。第二个弱点更致命,攻击者可以事先把常见密码组合成字典,批量算出对应的 MD5 值做成彩虹表,拿到你的密文后直接查表就能还原明文。像 123456、password 这类弱口令,在彩虹表里几乎是秒查。
加盐就是针对这两个弱点设计的。做法是在密码拼接一段随机字符串再计算摘要,这段随机字符串就是盐。由于每个用户的盐都不同,即使密码相同,最终存库的密文也完全不同;同时攻击者无法预先针对你的盐生成彩虹表,只能对每个用户单独暴力破解,破解成本呈数量级上升。注意盐并不需要保密,它可以和密文一起明文存储,它的作用是让攻击者的预计算失效,而不是充当密钥。
二、Spring Boot 中实现 MD5 加盐工具类
Spring 自带 DigestUtils 工具类,省去了自己封装 MessageDigest 的麻烦。我们先写一个工具类,包含生成随机盐和计算加盐摘要两个方法。盐建议使用 16 字节以上的随机数,再转成十六进制字符串保存。
import org.springframework.util.DigestUtils;
import java.security.SecureRandom;
public class PasswordUtils {
private static final SecureRandom RANDOM = new SecureRandom();
private static final char[] HEX = "0123456789abcdef".toCharArray();
/**
* 生成随机盐,返回16进制字符串
*/
public static String generateSalt() {
byte[] bytes = new byte[16];
RANDOM.nextBytes(bytes);
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(HEX[(b >> 4) & 0x0F]);
sb.append(HEX[b & 0x0F]);
}
return sb.toString();
}
/**
* 密码加盐后计算MD5,盐放在密码前后各拼一次,增加拼接复杂度
*/
public static String encrypt(String password, String salt) {
String raw = salt + password + salt;
return DigestUtils.md5DigestAsHex(raw.getBytes());
}
}
这里有两个细节值得说明。第一,随机数一定要用 SecureRandom 而不是 Random,后者的随机性在安全场景下是不够的。第二,盐的拼接位置不必拘泥于前后各拼一次,只要注册和登录两侧保持同一套规则即可,越不容易被猜到的拼接方式,防御效果越好。
三、注册与登录的完整流程实现
数据库表设计上,需要额外增加一个 salt 字段,用来存放每个用户的随机盐。建表语句如下:
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password CHAR(32) NOT NULL COMMENT 'MD5加盐后的密文',
salt VARCHAR(32) NOT NULL COMMENT '随机盐',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
注册逻辑的核心是先生成盐,再加密,最后把密文和盐一起入库。登录时则相反:根据用户名查出记录,取出库里的盐,用同样的方式加密用户输入的密码,比对密文是否一致。完整代码示例如下:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 注册:生成盐并加密入库
*/
public void register(String username, String password) {
String salt = PasswordUtils.generateSalt();
String encrypted = PasswordUtils.encrypt(password, salt);
SysUser user = new SysUser();
user.setUsername(username);
user.setPassword(encrypted);
user.setSalt(salt);
userMapper.insert(user);
}
/**
* 登录:取库中盐重新加密后比对
*/
public boolean login(String username, String password) {
SysUser user = userMapper.selectByUsername(username);
if (user == null) {
return false;
}
String encrypted = PasswordUtils.encrypt(password, user.getSalt());
// 恒定时间比较,避免时序攻击
return MessageDigest.isEqual(
encrypted.getBytes(),
user.getPassword().getBytes());
}
}
有一个容易被忽略的点:比对密文时建议用 MessageDigest.isEqual 而不是 equals。普通字符串比较一旦发现第一个不同的字符就立即返回,攻击者可以根据响应时间差异逐位猜测密文,这就是时序攻击。恒定时间比较无论哪里不同都耗时一致,可以从根本上堵住这个口子。
四、MD5 加盐的局限与更稳妥的替代方案
必须承认,MD5 加盐只能防住彩虹表,防不住高性能暴力破解。现代显卡每秒可以计算数十亿次 MD5,一个 8 位以内纯数字密码几乎瞬间就能被枚举出来。MD5 自身也早已被证明存在碰撞漏洞,安全敏感场景不建议继续使用。
更推荐的做法是使用专门的慢哈希算法,比如 BCrypt、SCrypt 或 Argon2。以 BCrypt 为例,它内置盐的生成和管理,计算成本可以随参数调节,Spring Security 对它有开箱即用的支持:
// Spring Security 提供的 BCrypt 加密器
@Bean
public PasswordEncoder passwordEncoder() {
// 参数10表示强度,越大越慢越安全,常用10到12
return new BCryptPasswordEncoder(10);
}
// 注册时加密,盐自动生成并包含在结果中
String encoded = passwordEncoder.encode("用户密码");
// 登录时校验,无需自己处理盐
boolean ok = passwordEncoder.matches("用户输入的密码", encoded);
可以看到 BCrypt 连盐字段都省了,密文本身自带盐信息,使用起来比手动加盐干净得多。如果你的项目已经引入了 Spring Security,直接用 PasswordEncoder 是最佳选择;如果是轻量项目不方便引入安全框架,MD5 加盐配合强制密码复杂度校验,也能达到一个及格线以上的安全水位。无论选哪种方案,切记不要让任何一个用户的密码以明文形式离开内存。
Spring BootMD5加盐密码加密存储修改时间:2026-09-07 19:36:37