【问题标题】:Spring Security - Random salt + Hash + Custom UserDetailsServiceSpring Security - 随机盐 + 哈希 + 自定义 UserDetailsS​​ervice
【发布时间】:2012-05-18 18:11:00
【问题描述】:

希望我的最后一个问题。到目前为止,我已经实现了我自己的自定义 UserDetails 和 UserDetailsS​​ervice 类,以便我可以传递在创建密码时使用的随机盐。密码的哈希是 SHA512。然而,在尝试登录时,我总是得到用户/密码组合不正确,我似乎无法弄清楚原因。

我将哈希和盐作为 blob 存储在数据库中,关于问题所在有什么想法吗?

Security-applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
  xmlns:sec="http://www.springframework.org/schema/security"
  xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <sec:http auto-config='true' access-denied-page="/access-denied.html">
        <!-- NO RESTRICTIONS -->        
        <sec:intercept-url pattern="/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />
        <sec:intercept-url pattern="/*.html" access="IS_AUTHENTICATED_ANONYMOUSLY"  /> 
        <!-- RESTRICTED PAGES -->
        <sec:intercept-url pattern="/admin/*.html" access="ROLE_ADMIN" />
        <sec:intercept-url pattern="/athlete/*.html" access="ROLE_ADMIN, ROLE_STAFF" />

        <sec:form-login login-page="/login.html"
                    login-processing-url="/loginProcess"
                    authentication-failure-url="/login.html?login_error=1"
                    default-target-url="/member" />
        <sec:logout logout-success-url="/login.html"/>
    </sec:http>

    <beans:bean id="customUserDetailsService" class="PATH.TO.CustomUserDetailsService"/>
    <beans:bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
        <beans:constructor-arg value="512"/>
    </beans:bean>

    <sec:authentication-manager>
        <sec:authentication-provider user-service-ref="customUserDetailsService">
            <sec:password-encoder ref="passwordEncoder"> 
                <sec:salt-source user-property="salt"/> 
            </sec:password-encoder>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans:beans>

CustomUserDetails.java

public class CustomUserDetails implements UserDetails {

    private int userID;
    private String username;
    private String password;
    private Collection<GrantedAuthority> authorities;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    private String salt;

    public CustomUserDetails() {
    }

    public CustomUserDetails(int userID, Collection<GrantedAuthority> authorities, String username, String password, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled, String salt) {
        this.userID = userID;
        this.authorities = authorities;
        this.username = username;
        this.password = password;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
        this.salt = salt;
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public int getUserID() {
        return userID;
    }

    public void setUserID(int userID) {
        this.userID = userID;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public String getSalt() {
        return salt;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setAuthorities(Collection<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }
}

CustomUserDetailsS​​ervice.java

public class CustomUserDetailsService implements UserDetailsService {

    private User_dao userDao;

    @Autowired
    public void setUserDao(User_dao userDao) {
        this.userDao = userDao;
    }

    @Override
    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
        MyUser myUser = new MyUser();
        myUser.setUsername(username);
        try {
            userDao.getUserByUsername(myUser);
        } catch (Throwable e) {
        }
        if (myUser == null) {
            throw new UsernameNotFoundException("Username not found", username);
        } else {
            List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
            authList.add(new GrantedAuthorityImpl(myUser.getUserRole().getAuthority()));

            int userID = myUser.getUserID();
            boolean accountNonExpired = true;
            boolean accountNonLocked = myUser.isNonLocked();
            boolean credentialsNonExpired = true;
            boolean enabled = myUser.isEnabled();
            String password = "";
            String salt = "";

            password = new String(myUser.getHash);
            salt = new String(myUser.getSalt());
            CustomUserDetails user = new CustomUserDetails(userID, authList, username, password, accountNonExpired, accountNonLocked, credentialsNonExpired, enabled, salt);
            return user;
        }
    }
}

密码创建

public byte[] generateSalt() throws NoSuchAlgorithmException {
    SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
    byte[] salt = new byte[20];
    random.nextBytes(salt);
    return salt;
}

public byte[] generateHash(byte[] salt, String pass) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-512");
    digest.update(salt);
    byte[] hash = digest.digest(pass.getBytes());
    return hash;
}

调用方法:

byte[] salt = generateSalt();
byte[] hash = generateHash(salt, password);
Which I then store in the db.

【问题讨论】:

    标签: spring spring-security


    【解决方案1】:

    我遇到了同样的原始问题,但从未得到解答,因此希望这可以为将来节省一些时间:

    Spring-Security 在比较摘要之前默认添加大括号。我错过了这一点并旋转了几个小时(d'oh)。

    确保存储(或生成)用大括号括起来的盐值(即,当 Spring 说“{salt}”时,它们真诚地表示“打开大括号 + 你的盐值 + 关闭大括号大括号'。

    我想这对大多数人来说是显而易见的,但直到我最终调试它时我才注意到它。

    【讨论】:

      【解决方案2】:

      我认为值得指出的是,将用于每个用户密码的 salt 存储在数据库的 salt 列中(虽然很常见)存在漏洞。加盐的原因首先是为了防止字典攻击受损的数据库。如果攻击者可以访问您的数据库并且没有使用盐,他们可以将通用哈希算法应用于标准字典中的每个单词,以创建新的哈希字典。当他们在数据库中找到其中一个词的匹配项时,他们会查阅自己字典中的映射,以找到在应用算法时产生该哈希的原始未哈希词。瞧!攻击者有密码。

      现在...如果您使用盐并且每个用户的盐都不相同,那么您会在该攻击计划中投入巨大的活动扳手。但是...如果您为数据库中的每个用户存储盐(并通过将列命名为“盐”来使其显而易见),那么您实际上并没有过多地干扰这个攻击计划。

      这是我所知道的最安全的方法。

      1. 您的用户表中没有盐列。
      2. 在您的 UserDetails 类上实现 getSalt() 方法。让它返回用户注册时设置的其他用户属性,并且永远不会更改。例如加入日期。将其与在 UserDetails 类中硬编码的字符串文字/常量连接起来。

      通过这种方式,盐对每个用户都是唯一的。从数据库中看不出什么值被用作盐。即使攻击者猜到了部分盐的用途,他/她也需要访问您的源代码以了解盐的 REST。这意味着您的数据库和应用程序代码都需要在您遇到真正的问题之前受到损害。

      假设您了解我刚才所说的所有内容的优点,这里还有一个相当大的优势,即我刚才所说的实际上比您已经在做的更容易实施。当正确的事情变得更容易时,我会喜欢它!

      【讨论】:

      • 如果他们有源代码,他们不知道盐是如何生成的吗?他们会从代码中获得字符串文字,并且他们会知道数据库中的字段。如果像加入日期或用户 ID 这样容易破解的话,更不用说在网站上查找了。无论如何,即使我确实按照您提到的那样实现了它,但这仍然不能解决我的 SpringSecurity 没有正确验证密码的问题。是变量类型 byte[] 的问题吗?
      • @Kent “首先加盐的原因是为了防止字典攻击受损的数据库。”这是错误的,盐是用于防止预计算攻击,而不是字典攻击。它还可以防止帐户之间的共享密码。如果密码被破解,您只能访问该帐户,而不是该帐户以及与该人使用相同密码的每个人。您基本上是在通过默默无闻来倡导安全。
      • @Felix,你是对的。如果有人可以看到源和数据库,他们可以找出盐,但这比将盐存储在数据库中要好,因为这样某人只需访问数据库即可了解给定用户的 SALT。回复:您最初的问题-对不起,我没有回答。再看一遍...... SS 和数据库之间在哈希密码是什么方面存在分歧。解决这种分歧的最好方法是让将密码写入数据库的代码使用 passwordEncoder bean 来计算散列。
      • @Scott,如果我错了,那只是“字典”攻击标签。然而细节是正确的。如果您将盐存储在用户表中的 CALLED salt 列中,您刚刚通知了一个已经破坏您的数据库的被攻击者,该用户的密码使用了什么盐。现在他们只需要从少数算法中猜测,他们就可以执行您的“预先计算的攻击”,这可能会为单个给定用户生成密码。当盐源不明显并在数据库和代码之间分裂时,该攻击向量就关闭了。
      • (而且我知道“默默无闻的安全性”是不好的。但这实际上是对不要实施“仅通过默默无闻的安全性”的告诫。在任何彻底的安全性中,隐藏敏感信息是一个(许多)合法组件解决方案。如果你怀疑我,我问你什么是散列或加密值......它本身不是模糊信息吗?)
      猜你喜欢
      • 2013-02-11
      • 2013-05-12
      • 2014-10-12
      • 2014-10-06
      • 2012-12-13
      • 2016-03-09
      • 1970-01-01
      • 1970-01-01
      • 2012-06-11
      相关资源
      最近更新 更多