在Shiro 1.3.0中实施散列密码验证时遇到问题

时间:2015-01-17 22:15:47

标签: apache jsf authentication jsf-2.2 shiro

好的 - 我一直在这里旋转我的车轮一周试图弄清楚这一点,我已经无处可去了。 Shiro文档/教程似乎在各种版本和不赞成的设计范例之间过于分散,因此我很难弄清楚如何自己实现这一点。即使是官方文档也留下了许多空白,让新手感到茫然和困惑。我一直在关注这个(现在有点过时)BalusC教程,在JSF应用程序中设置Apache Shiro身份验证(链接:http://balusc.blogspot.sg/2013/01/apache-shiro-is-it-ready-for-java-ee-6.html)。我能够通过“Hashing the password”部分关注这篇文章,这篇文章真的开始显示它的年龄(声称不支持salting),并驱使我去其他地方寻找实施这个关键特征。

以下是我的开发环境的基础知识。

IDE: NetBeans 8.0.2
服务器: TomEE 1.7.1
DB: MySQL 5.5
应用框架:
JSF 2.2
OmniFaces 1.8.1(尚未发挥作用,除了我使用o:form标签以期待进一步使用)
JPA 2.0(正如TomEE 1.7.1声称不支持2.1,至少这是NetBeans告诉我的......) Apache DeltaSpike 1.2.1(仅限核心和JPA模块;协助CDI + JPA持久性)
Apache Shiro 1.3.0-SNAPSHOT(仅限核心和Web模块)

我能够开展工作:
- 从MySQL数据库验证用户;读取原始/纯文本用户名和密码
- 注册表单以原始/纯文本格式将新用户保存到用户表

这里有相关配置:
shiro.ini

[main]
# As per BalusC's guide for Ajax aware custom User Filter. Do not use @WebFilter annotation or web.xml to register this filter!
user = ds.nekotoba.filter.FacesAjaxAwareUserFilter
user.loginUrl = /faces/public/login.xhtml

#Define Realm
jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.permissionsLookupEnabled = true
jdbcRealm.authenticationQuery = SELECT password FROM users WHERE username = ?
jdbcRealm.userRolesQuery = SELECT role FROM userroles WHERE user_id = (SELECT id FROM users WHERE username = ?)

#Define Datasource
dataSource = com.mysql.jdbc.jdbc2.optional.MysqlDataSource
dataSource.serverName = localhost
dataSource.port = ####
dataSource.user = ********
dataSource.password = ********
dataSource.databaseName = ********
jdbcRealm.dataSource = $dataSource

#Add realm to securityManager
securityManager.realms = $jdbcRealm

[urls]
/faces/javax.faces.resource/** = anon
/faces/public/login.xhtml = user
/faces/public/app/** = user

用户创建方法 create()

public Long create(User user) {
    //Set created date
    user.setCreatedDate(new Date());

    //Persist to DB
    em.persist(user);
    return user.getId();
}

程序化用户登录方法(来自自定义JSF登录表单)login()

public void login() throws IOException {
    try {
        SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password, remember));
        SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(Faces.getRequest());
        Faces.redirect(savedRequest != null ? savedRequest.getRequestUrl() : Globals.HOME_URL);
    } catch (AuthenticationException ae) {
        Messages.addGlobalError("不明なユーザ、また試してみてください。");
        ae.printStackTrace();
    }
}

这一切都非常简单,并且有效 - 但是当我尝试将所有这些转换为使用散列密码时,我的问题就出现了。我能够从修改后的创建方法获得一个哈希密码,并将相关的盐保存到数据库中 - 但我似乎无法在我的shiro.ini中获得正确的设置以使用这些哈希用户成功登录。

更新了shiro.ini(以实现新的哈希密码匹配)。请注意添加的PasswordServicePasswordMatcher和已更新的authenticationQuery,其中包含salt列。为简洁起见,我只包含了ini文件的更新部分。

#Include salt column in authentication query
jdbcRealm.authenticationQuery = SELECT password, salt FROM users WHERE username = ?

#Use Default PasswordService/Matcher. This should use SHA-256, with 500,000 iteration hashing.
passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
passwordMatcher.passwordService = $passwordService
jdbcRealm.credentialsMatcher = $passwordMatcher

更新了create()方法,该方法哈希密码,并提取盐以便在数据库中单独存储。

public Long create(User user) {
    HashingPasswordService hps = new DefaultPasswordService();
    //Hash password given in registration form using the DefaultPasswordService,
    //this should use the same defaults at the PasswordService/Matcher defined in shiro.ini
    Hash hash = hps.hashPassword(user.getPassword());
    //Set user.password to hashed version for persisting to DB
    String hashedPass = hash.toBase64();
    user.setPassword(hashedPass);
    //Get related salt from hash and set in user object for persisting to DB
    String salt = hash.getSalt().toBase64();
    user.setSalt("salt");

    //Set created date
    user.setCreatedDate(new Date());

    //Persist to DB
    em.persist(user);
    return user.getId();
}

这就是我遇到的问题 - 我假设此时我需要更新登录方法,以便用户通过从数据库中检索盐来处理输入密码的哈希,并对提交的密码进行哈希处理,然后进行匹配来自数据库的存储哈希密码 - 但我尝试过的所有尝试都没有成功。尝试使用上面的login()方法进行身份验证只会让我错误地说明提供的凭据不匹配...

我已经尝试在这个(日语)教程(日本语が少し知っていますよ)中实现了一些代码,但对我来说似乎有些偏差(链接:http://d.hatena.ne.jp/apyo/touch/20120603/1338739210)。 Shiro不支持散列/腌制吗?我不明白为什么我必须重写/覆盖Shiro方法(根据该教程)来完成对散列数据的用户身份验证,这是声称框架支持的东西。我认为有一些我应该使用的方法,我找不到合适的方法。当然,可能是我散列/存储/检索用户数据的方式导致了问题......我对框架的新知识太新了。任何帮助将不胜感激。

修改 好的 - 似乎也许是#34;标准"必须覆盖已传递的JdbcRealm中的方法以实现用户密码+ salt的验证。我已经实现了以下自定义域,但在调用该方法时得到NullPointerException

CustomJdbcRealm.java (基于以上链接教程)

import java.util.List;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SaltedAuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.DefaultPasswordService;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.util.ByteSource;

/**
 * @author mousouchop
 *
 * Modification Log: 2015-01-17 Original.
 */
public class CustomJdbcRealm extends JdbcRealm {
    @Inject
    private EntityManager em;

    /**
     * 認証情報を返却する。
     * @param token
     * @return 
     */
    @Override
    protected SaltedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
        throws AuthenticationException {

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        SaltedAuthenticationInfo info = null;

        System.out.println("TEST######## TOKEN-UN: "+upToken.getUsername());

        // Here I switched out a plain JDBC query for a JPA query (Perhaps this is part of my problem?)
        List<String> queryResults = em.createNamedQuery("User.ps")
                .setParameter("username", username)
                .getResultList();
        System.out.println("TEST######## RESULT CT: "+queryResults.size());
        String password = queryResults.get(0);
        String salt = queryResults.get(1);
        System.out.println("TEST######## PW: "+password);
        System.out.println("TEST######## SLT: "+salt);

        // ShiroデフォルトのjdbcRealmをそのまま使い、SecurityManagerをshiro.iniで初期化する方法だと、
        // saltStyleが設定できない。saltをAuthenticationInfoに渡す方法は用意されているが、
        // DefaultPasswordServiceを使うとバグ?でsaltが無視されてしまう(まだ調査中です)
        // とりあえずブログ用に強引にゴリゴリとHashクラスを生成する。
        // ハッシュアルゴリズムが固定になってしまっているが、
        // まぁよしとする。別に動的に生成しなければいけない場面もないだろうし。
        Sha256Hash credentials = Sha256Hash.fromBase64String(password);
            credentials.setSalt(ByteSource.Util.bytes(Base64.decode(salt)));
        // SimpleHashクラスとDefautoPasswordServiceでハッシュ回数のデフォルト値が異なる。
        // 整合性が取れてないとも言えるが、これはこれでいいのかな。
    credentials.setIterations(DefaultPasswordService.DEFAULT_HASH_ITERATIONS);
        // principals,credentials,realm名を設定して、AuthenticationInfoを生成する。
        info = new SimpleAuthenticationInfo(username, credentials, getName());

        return info;
    }
}

&#34; TEST&#34;我有输出行 - 第一个返回输入的用户名...我在其他任何人打印之前得到NPE。也许我会尝试在这一个类中使用普通的JDBC调用......

1 个答案:

答案 0 :(得分:0)

好的......我做了一点挖掘,似乎我不能像上次尝试的那样直接使用CDI注入到Shiro JDBC领域......管理层之间存在冲突拥有它自己的豆子,CDI拥有自己的豆子。我确实发现this answer似乎试图通过初始化领域在CDI和Shiro之间建立桥梁,将用户DAO从CDI插入领域,然后将领域实例绑定到JNDI以便稍后在{{{但是,再一次,所有的例子/文档都是如此分散,以至于我无法完全发挥作用。

因此,我刚才使用了我在问题中列出的日语教程中的自定义shiro.ini。我不想在CDI之外偏离我的bean中的数据库交互,但因为已经在JdbcRealm中定义了JDBC数据源,所以所有连接信息和查询都从那里直接拉入而不需要在自定义领域硬编码/重新定义它们。至少我没有重复的代码重复...这里是为了完整起见,我的最终,工作,自定义shiro.ini bean粘贴在这里:

<强> CustomJdbcRealm.java

JdbcRealm

因此,这与我上面的问题中的更新 import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SaltedAuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.DefaultPasswordService; import org.apache.shiro.codec.Base64; import org.apache.shiro.crypto.hash.Sha256Hash; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.util.ByteSource; import org.apache.shiro.util.JdbcUtils; /** * @author mousouchop * * Modification Log: 2015-01-17 Original. */ public class CustomJdbcRealm extends JdbcRealm { /** * 認証情報を返却する。(Override AuthenticationInfo method) */ @Override protected SaltedAuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); Connection conn = null; SaltedAuthenticationInfo info = null; try { conn = dataSource.getConnection(); // DBよりパスワード、SALTを取得する。(Get password and salt from DB) String[] queryResults = getPasswordForUser(conn, username); String password = queryResults[0]; String salt = queryResults[1]; // ShiroデフォルトのjdbcRealmをそのまま使い、SecurityManagerをshiro.iniで初期化する方法だと、 // saltStyleが設定できない。saltをAuthenticationInfoに渡す方法は用意されているが、 // DefaultPasswordServiceを使うとバグ?でsaltが無視されてしまう(まだ調査中です) // とりあえずブログ用に強引にゴリゴリとHashクラスを生成する。 // ハッシュアルゴリズムが固定になってしまっているが、 // まぁよしとする。別に動的に生成しなければいけない場面もないだろうし。 Sha256Hash credentials = Sha256Hash.fromBase64String(password); credentials.setSalt(ByteSource.Util.bytes(Base64.decode(salt))); // SimpleHashクラスとDefautoPasswordServiceでハッシュ回数のデフォルト値が異なる。 // 整合性が取れてないとも言えるが、これはこれでいいのかな。 credentials.setIterations(DefaultPasswordService.DEFAULT_HASH_ITERATIONS); // principals,credentials,realm名を設定して、AuthenticationInfoを生成する。 info = new SimpleAuthenticationInfo(username, credentials, getName()); } catch (SQLException e) { final String message = String.format("次のユーザーの認証中にSQLエラーが発生しました。ユーザー:[%s]%n", username); throw new AuthenticationException(message, e); } finally { JdbcUtils.closeConnection(conn); } return info; } /** * DBよりユーザーに紐づくパスワードとSALTを取得する。 org.apache.shiro.realm.jdbc.JdbcRealmよりコピー。 * saltStyleに関する記述を除去。 * * @param conn * @param username * @return * @throws SQLException */ private String[] getPasswordForUser(Connection conn, String username) throws SQLException { String[] result; result = new String[2]; PreparedStatement ps = null; ResultSet rs = null; try { // 親クラスにデフォルトで用意されているSQLか、 // shiro.iniにもパスワード取得用SQLを定義できる。 ps = conn.prepareStatement(authenticationQuery); ps.setString(1, username); rs = ps.executeQuery(); // 複数レコードが取れた場合のチェックフラグ boolean foundResult = false; while (rs.next()) { // ループ2周めだとfoundResultはtrue=エラー if (foundResult) { throw new AuthenticationException( String.format("ユーザー:%sのデータが複数あります。ユーザー名に対応するユーザーは必ず一人でないといけません。%n", username)); } // password result[0] = rs.getString(1); // password_salt result[1] = rs.getString(2); foundResult = true; } } finally { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(ps); } return result; } } shiro.ini方法一起应该允许您使用Apache Shiro 1.2。* / 1.3实现散列用户身份验证。 0快照。