在用户认证系统中,密码的加密存储是保障数据安全的第一道防线。Apache Shiro 提供了灵活的加密模块,支持 MD5、SHA 等多种算法,并通过**盐值(Salt)**机制有效抵御。本文将结合实际代码,详细讲解如何在 Shiro 中实现 MD5 + 随机盐值 的密码加密方案。
一、为什么需要盐值加密?
1. 传统 MD5 的安全隐患
- 可通过预计算的 MD5 哈希值表(如 CrackStation)快速反查密码。
- 相同密码哈希值相同:若多个用户使用相同密码(如
123456
),其 MD5 哈希值完全一致,增加泄露风险。
2. 盐值的作用
- 唯一性:为每个用户的密码生成随机盐值(如
"abc123"
),密码加密时拼接盐值(password + salt
)。 - 不可逆性:即使两个用户密码相同,由于盐值不同,最终哈希值也不同。
- 需为每个盐值单独生成彩虹表,成本极高。
二、Shiro 中 MD5 + 盐值加密的实现步骤
1. 添加 Shiro 依赖
在 Maven 项目中引入 Shiro 核心库:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.12.0</version> <!-- 使用最新版本 -->
</dependency>
2. 生成随机盐值
Shiro 的 SimpleHash
类支持直接传入盐值参数:
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
public class PasswordUtil {
// 生成随机盐值(16位十六进制字符串)
public static String generateSalt() {
return new SecureRandomNumberGenerator().nextBytes().toHex();
}
// MD5 + 盐值加密
public static String encryptPassword(String plainPassword, String salt) {
// 参数:算法名、明文密码、盐值、迭代次数(可选)
SimpleHash hash = new SimpleHash("MD5", plainPassword, ByteSource.Util.bytes(salt), 1024);
return hash.toHex(); // 返回16进制哈希值
}
}
关键点:
SecureRandomNumberGenerator
生成高强度随机盐值(如"3a7f3b0c8d1e2f4a"
)。ByteSource.Util.bytes(salt)
将盐值转换为字节数组。
3. 用户注册时的密码加密
在用户注册时,生成盐值并存储密码哈希值:
public class UserService {
public void registerUser(String username, String plainPassword) {
// 1. 生成随机盐值
String salt = PasswordUtil.generateSalt();
// 2. 加密密码
String encryptedPassword = PasswordUtil.encryptPassword(plainPassword, salt);
// 3. 存储到数据库(示例:User表包含username、password、salt字段)
User user = new User();
user.setUsername(username);
user.setPassword(encryptedPassword); // 存储加密后的哈希值
user.setSalt(salt); // 存储盐值(通常与密码哈希值分开存储)
userDao.save(user);
}
}
4. 用户登录时的密码验证
登录时,从数据库获取盐值并验证密码:
public class AuthService {
public boolean authenticate(String username, String plainPassword) {
// 1. 从数据库查询用户(假设已实现)
User user = userDao.findByUsername(username);
if (user == null) {
return false; // 用户不存在
}
// 2. 获取存储的盐值和密码哈希值
String salt = user.getSalt();
String storedPassword = user.getPassword();
// 3. 使用相同算法和盐值重新加密输入密码
String encryptedInput = PasswordUtil.encryptPassword(plainPassword, salt);
// 4. 比较哈希值,改用常量时间比较
return encryptedInput.equals(storedPassword); // 或使用 Shiro 的 SecurityUtils.getSecurityManager().assert(condition)
}
}
安全提示:
- 实际项目中建议使用
MessageDigest.isEqual()
或 Shiro 内置的ByteSource.equals()
进行常量时间比较。
三、数据库设计建议
字段名 | 类型 | 示例值 | 说明 |
| VARCHAR(50) | "john_doe" | 用户名 |
| VARCHAR(128) | "d41d8cd98f00b204e9800998ecf8427e" | MD5哈希值(32字符16进制) |
| VARCHAR(32) | "3a7f3b0c8d1e2f4a" | 随机盐值 |
四、完整代码示例
1. 测试类
public class ShiroMd5SaltTest {
public static void main(String[] args) {
String plainPassword = "MySecurePassword123";
// 1. 生成盐值
String salt = PasswordUtil.generateSalt();
System.out.println("Salt: " + salt); // 示例输出: Salt: 3a7f3b0c8d1e2f4a
// 2. 加密密码
String encrypted = PasswordUtil.encryptPassword(plainPassword, salt);
System.out.println("Encrypted Password: " + encrypted); // 示例输出: 32字符MD5哈希
// 3. 验证密码
boolean isValid = encrypted.equals(PasswordUtil.encryptPassword(plainPassword, salt));
System.out.println("Password valid: " + isValid); // 输出: true
}
}
2. 输出结果
Salt: 3a7f3b0c8d1e2f4a
Encrypted Password: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
Password valid: true
五、进阶优化
1. 使用更安全的算法
- 推荐算法:
SHA-256
或PBKDF2WithHmacSHA256
(需调整SimpleHash
的第一个参数)。 - 示例:
// 使用SHA-256替代MD5
SimpleHash hash = new SimpleHash("SHA-256", plainPassword, ByteSource.Util.bytes(salt), 1024);
2. 结合 Shiro 的 Realm
在自定义 Realm
中直接使用 HashedCredentialsMatcher
:
public class CustomRealm extends AuthorizingRealm {
public CustomRealm() {
// 配置凭证匹配器:MD5算法、1024次迭代、存储16进制哈希
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("MD5");
matcher.setHashIterations(1024);
matcher.setStoredCredentialsHexEncoded(true);
setCredentialsMatcher(matcher);
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// 从数据库查询用户(略)
User user = userDao.findByUsername(username);
if (user == null) {
throw new UnknownAccountException();
}
// 返回AuthenticationInfo(包含盐值和加密后的密码)
return new SimpleAuthenticationInfo(
username,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName()
);
}
}
六、总结
通过本文的实战案例,我们实现了以下核心功能:
- 使用 MD5 + 随机盐值 加密用户密码。
- 在用户注册时生成盐值并存储密码哈希值。
- 在用户登录时通过盐值验证密码。
- 结合 Shiro 的
HashedCredentialsMatcher
简化代码。
安全建议:
- 避免使用 MD5,推荐升级到 SHA-256 或 PBKDF2。
- 盐值长度建议 ≥ 16 字节(128位)。
- 定期轮换盐值(如用户修改密码时)。
完整代码已上传至 GitHub示例仓库,欢迎 Star 和 Fork!