0
点赞
收藏
分享

微信扫一扫

Shiro 实战:MD5 盐值加密实现用户密码安全存储

在用户认证系统中,密码的加密存储是保障数据安全的第一道防线。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() 进行常量时间比较。

三、数据库设计建议

字段名

类型

示例值

说明

username

VARCHAR(50)

"john_doe"

用户名

password

VARCHAR(128)

"d41d8cd98f00b204e9800998ecf8427e"

MD5哈希值(32字符16进制)

salt

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-256PBKDF2WithHmacSHA256(需调整 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()
        );
    }
}

六、总结

通过本文的实战案例,我们实现了以下核心功能:

  1. 使用 MD5 + 随机盐值 加密用户密码。
  2. 在用户注册时生成盐值并存储密码哈希值。
  3. 在用户登录时通过盐值验证密码。
  4. 结合 Shiro 的 HashedCredentialsMatcher 简化代码。

安全建议

  • 避免使用 MD5,推荐升级到 SHA-256 或 PBKDF2。
  • 盐值长度建议 ≥ 16 字节(128位)。
  • 定期轮换盐值(如用户修改密码时)。

完整代码已上传至 GitHub示例仓库,欢迎 Star 和 Fork!

举报

相关推荐

0 条评论