首页 > 解决方案 > 如何使用 Apache Shiro Credential Matcher 匹配密码哈希结果

问题描述

我正在使用 Apache Shiro 创建 Web 应用程序用户身份验证功能。我有从已发布的 Shiro 教程示例改编的用户登录和主页 JSP。这些在 Apache Tomcat 中执行。Shiro 配置了一个 JDBC 领域,并且在与 Shiro 示例匹配的原型 MySQL 数据库中的用户、角色和权限表上的默认身份验证查询。密码最初是明文,数据库用户表包括一个尚未实现的盐列。在这种状态下,使用这些组件的登录和身份验证可以正常工作并且符合预期。

我现在正在尝试实现散列和加盐密码。我的意图是每个用户密码都有自己的盐存储在数据库中。我正在使用 SHA-256 哈希并将结果存储为 Base64 格式。这是 shiro.ini 段,它尝试定义此配置,特别是凭证匹配器,以使用来自数据库列的盐执行所需的密码散列:

# Configure the MySQL data source
ds = org.apache.commons.dbcp.BasicDataSource
ds.driverClassName = com.mysql.cj.jdbc.Driver
ds.username = ****
ds.password = ****
ds.url=jdbc:mysql://localhost:3306/shiro_users

# Use salted and hashed SHA-256 passwords stored in Base64 format
credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName = SHA-256
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashSalted = true
 
# Configure the JdbcRealm   
jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $ds
jdbcRealm.credentialsMatcher = $credentialsMatcher
jdbcRealm.permissionsLookupEnabled = true
jdbcRealm.saltStyle = SaltStyle.COLUMN
jdbcRealm.authenticationQuery = SELECT password, salt from users WHERE username = ?
jdbcRealm.userRolesQuery = SELECT role_name FROM user_roles WHERE user_name = ?
jdbcRealm.permissionsQuery = SELECT permission FROM roles_permissions WHERE role_name = ?

securityManager.realms = $jdbcRealm

为了使用散列密码和相应的盐值填充用于开发的用户数据库,我创建了一个原型 Java 密码类。我从已发布的示例中调整了一组方法来生成盐和散列明文密码值。请注意,哈希密码和盐值是以 Base64 格式创建并存储在数据库中的。这旨在与以下的 shiro.ini 设置保持一致:

credentialsMatcher.storedCredentialsHexEncoded = false

对于 Password 类的现阶段开发,散列的密码和盐值被打印到控制台,以便手动粘贴到适当的数据库列中,替换原本可以正常工作的明文值。

下面是java Password类的关键方法:

public class Password {
    
    /**
     * saveHashedPassword
     * 
     * Generates salt and hashes the given clear text password. Both the hashed password
     * and salt are encoded in Base64 and saved to the user database. 
     * 
     * Note: In this development version, the password and salt Base64 strings are 
     * printed to the console for manual pasting into the appropriate database columns.
     * 
     * @param clearTextPassword The clear text password to be hashed and salted
     */
    public void saveHashedPassword (String clearTextPassword) {
        String hashedPassword;
        byte[] saltBytes;
        String salt;
        
        saltBytes = getSalt();
        hashedPassword = hashAndSaltPassword(clearTextPassword, saltBytes);
        salt = Base64.getEncoder().encodeToString(saltBytes);
        // persist salt and hash or return them to delegate this task to other component
        System.out.println(hashedPassword.length() + "\tpwd \t" + hashedPassword);
        System.out.println(salt.length() + "\tsalt\t" + salt);
    }
    
    /**
     * hashAndSaltPassword
     * 
     * Generates a hashed and salted Base64 password from a clear text password and
     * salt byte array.
     * 
     * @param clearTextPassword The clear text password to hash and salt
     * @param salt The byte array salt value to use in hashing
     * @return The hashed and salted password
     */
    private String hashAndSaltPassword(String clearTextPassword, byte[] salt) {
        byte[] hashedBytes;
        String hashedPassword = "";
        
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(salt);
            hashedBytes = digest.digest(clearTextPassword.getBytes(StandardCharsets.UTF_8));
            hashedPassword = Base64.getEncoder().encodeToString(hashedBytes);
        } catch (NoSuchAlgorithmException e) {
            System.out.println(e.getMessage());
        }
        
        return hashedPassword;
    }
    
    /**
     * getSalt
     * 
     * Generates a salt byte array
     * 
     * @return A byte array salt value
     */
    private byte[] getSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        return salt;
    }
}

对于“secret”的明文密码,Password,hashAndSaltPassword() 返回以下值(打印到控制台并手动粘贴到相应的数据库列中):

Password:   mKYfzHXt4kHoLgzc7/C2upKXuCZ0SSGjUrnGIqzd92E=
Salt:       bigG9fGM9mjcqNiprFQ4+g==

当 Web 应用程序使用哈希密码 Shiro.ini 配置以及上述数据库中的哈希密码和 salt 值执行时,登录过程将失败。(在应用密码哈希之前它工作得很好。)没有抛出异常,Shiro 和 Tomcat 都没有给出除了登录失败之外的任何诊断。正常的应用程序日志中不会报告任何错误。

Shiro 在运行时对明文密码进行哈希处理的结果与 Password.hashAndSaltPassword() 方法生成的数据库中存储的哈希密码和盐值似乎不匹配。我认为 Shiro 凭证匹配器的配置与创建存储的散列和加盐密码的方法之间存在一些不一致的地方,但我无法进一步诊断问题或找到一个完全阐明用例的具体示例。

非常感谢任何诊断问题的帮助、推荐适当的资源或其他建设性的意见或建议。非常感谢。

标签: javamysqlhashshiro

解决方案


推荐阅读