用于基本身份验证的 Apache Shiro 凭据匹配总是失败

Posted

技术标签:

【中文标题】用于基本身份验证的 Apache Shiro 凭据匹配总是失败【英文标题】:Apache Shiro credentials matching for basic authentication always fails 【发布时间】:2015-09-07 19:05:18 【问题描述】:

我正在尝试使用 Apache Shiro 实现基本身份验证。我将监听器和过滤器添加到我的 web.xml 文件中:

<listener>
 <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
 <filter-name>ShiroFilter</filter-name>
 <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
 <filter-name>ShiroFilter</filter-name>
 <url-pattern>/*</url-pattern>
 <dispatcher>REQUEST</dispatcher>
 <dispatcher>FORWARD</dispatcher>
 <dispatcher>INCLUDE</dispatcher>
 <dispatcher>ERROR</dispatcher>
</filter-mapping>

并将 authcBasic 过滤器应用于我的 shiro.ini 文件中的受保护页面:

[urls]
/api/users = anon
/login.html = anon
/** = authcBasic

另外,我使用 Sha256CredentialsMatcher 和 JdbcRealm 子类作为领域:

[main]
jdbcRealm = my.package.SaltColumnJDBCRealm
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
sha256Matcher.storedCredentialsHexEncoded = false
sha256Matcher.hashIterations = 1024 
jdbcRealm.credentialsMatcher = $sha256Matcher
jdbcRealm.authenticationQuery = SELECT password, salt FROM User WHERE email = ?
builtInCacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $builtInCacheManager
securityManager.realms = $jdbcRealm

领域类:

public class SaltColumnJDBCRealm extends org.apache.shiro.realm.jdbc.JdbcRealm

    public SaltColumnJDBCRealm()
    
        this.setSaltStyle(SaltStyle.COLUMN);
    

可以通过将 JSON 对象发布到 RESTful 网络服务来将用户添加到数据库中:

import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.util.ByteSource;

@Path("/users")
@RequestScoped
public class UserService

    @EJB
    UserDao userDao;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public void register(User user)
    
        try
        
            encryptPassword(user);
            userDao.persist(user);
        
        catch(InvalidEntityException | UnsupportedEncodingException e)
        
            throw new WebApplicationException(e);
        
    

    private static void encryptPassword(User user) throws UnsupportedEncodingException
    
        RandomNumberGenerator rng = new SecureRandomNumberGenerator();
        ByteSource salt = rng.nextBytes();

        String hashedPasswordBase64 = new Sha256Hash(user.getPassword(), salt, 1024).toBase64();
        user.setPassword(hashedPasswordBase64);
        user.setSalt(salt.toBase64());
    

(这几乎是在tutorial from the Shiro web page之后。)

这种新用户注册工作正常,当客户端尝试进行身份验证时,用户名和密码可以毫无问题地到达服务器。但是后来登录总是失败。

我已经通过调试到以下几点来追踪问题: Sha256CredentialsMatcher 从数据库中检索与接收到的用户名匹配的盐,并使用该盐对接收到的密码进行哈希处理。然后,它将结果与已存储在数据库中的该用户名的密码散列进行比较。只是在我的情况下,这些哈希永远不会匹配,即使客户端发送了正确的用户名和密码组合。看起来好像盐和/或密码哈希在将它们存储在数据库中并再次检索它们的过程中发生了变化。或者好像Sha256Hash(...) 构造函数和Sha256CredentialsMatcher.doCredentialsMatch(...) 方法在散列相同密码时会得出不同的结果。但我不知道这会如何发生。

我已经尝试在数据库中将盐存储为salt.toString() 而不是salt.toBase64()

public class UserService

    ... 
    private static void encryptPassword(User user) throws UnsupportedEncodingException
    
        ... 
        user.setSalt(salt.toString());
...

两种变体都会出现错误。

【问题讨论】:

【参考方案1】:

我使用以下方法,它可以工作

credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=SHA-256
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024
credentialsMatcher.hashSalted = true

您的实现中缺少credentialsMatcher.**hashSalted = true** 部分。

【讨论】:

我试过这个配置,可惜没有成功。由于我的 Shiro 版本(1.2.3)不推荐使用 credentialsMatcher.hashSalted 属性,因此我尝试了使用和不使用此属性,但均未成功。无论如何,您的帖子向我指出了 Sha256CredentialsMatcher 也已弃用的事实,因此我将继续使用 HashedCredentialsMatcher 从现在开始。但匹配问题依然存在。【参考方案2】:

将近三个月后,我找到了解决方案。 :-/ 与 Shiro 文档中的教程相反,您不能简单地实例化一个 ByteSource 对象并将其用作盐(至少在 1.2.3 版本中不是)。相反,您需要从 ByteSource 对象中获取一个字符串并将其用作盐。我选择获取 Base64 编码的字符串,所以我的 encryptPassword 方法现在看起来像这样:

private static void encryptPassword(User user) throws UnsupportedEncodingException

    RandomNumberGenerator rng = new SecureRandomNumberGenerator();
    ByteSource byteSource = rng.nextBytes();
    String salt = byteSource.toBase64();
    String hashedPasswordBase64 = new Sha256Hash(user.getPassword(), salt, 1024).toBase64();
    user.setPassword(hashedPasswordBase64);
    user.setSalt(salt);

【讨论】:

以上是关于用于基本身份验证的 Apache Shiro 凭据匹配总是失败的主要内容,如果未能解决你的问题,请参考以下文章

Apache Shiro - 用于身份验证和属性的 LDAP/用于授权的 Ini

Apache Shiro 使用手册Shiro 认证

Cas(用于身份验证)+ OpenLDAP(用于用户名、密码+角色和权限)+ Apache Shiro(用于授权)

Apache Shiro

需要重写或实现哪些类或方法才能仅自定义 Shiro 的身份验证逻辑?

使用 Facebook OAuth 进行 Apache Shiro 身份验证