Spring Security - Random salt + Hash + Custom UserDetailsS​​ervice

时间:2012-05-18 18:11:00

标签: spring spring-security

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

我将数据库中的哈希和salt存储为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.

2 个答案:

答案 0 :(得分:3)

我遇到了同样的原始问题,这个问题从来没有得到解决,所以希望这可能会节省一些时间:

Spring-Security在比较摘要之前默认添加大括号。我错过了那个并且把我的车轮旋转了好几个小时(哦哦)。

确保存储(或生成)用大括号括起来的盐值(例如,当Spring说'{salt}'时,他们真诚地表示'开放大括号+你的盐值+接近卷曲大括号。

我认为这对大多数人来说是显而易见的,但在我最终调试之前我没有注意到它。

答案 1 :(得分:-3)

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

现在......如果您使用盐并且每个用户的盐不同,您会在该攻击计划中投入大量的猴子扳手。但是......如果你为数据库中的每个用户存储盐(并通过命名“盐”列来明确表示),那么你实际上并没有对这个攻击计划产生过多干扰。

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

  1. 您的用户表中没有盐列。
  2. 在UserDetails类上实现getSalt()方法。让它返回用户注册时设置的其他用户属性,并且永远不会更改。例如加入日期。使用在UserDetails类中硬编码的字符串文字/常量连接它。
  3. 以这种方式,盐对每个用户都是独一无二的。从数据库中看,用作盐的价值不明显。即使攻击者猜到了部分盐的用途,他/她也需要访问您的源代码才能知道盐的REST。这意味着您的数据库和应用程序代码在您最终遇到真正的问题之前需要妥协。

    假设你理解我刚才所说的一切的优点,这里也有一个相当大的优势,我刚才所说的实际上比你已经做的更容易实现。当正确的事情变得更容易时,喜欢它!