如何在Java中散列密码?

How can I hash a password in Java?

我需要散列密码以便存储在数据库中。如何在Java中做到这一点?

我希望获得纯文本密码,添加一个随机的salt,然后将salt和散列密码存储在数据库中。

然后,当一个用户想要登录时,我可以获取他们提交的密码,从他们的帐户信息中添加随机salt,散列它,看看它是否等于存储的散列密码和他们的帐户信息。


实际上,您可以使用内置到Java运行时的设备来执行此操作。Java 6中的SunJCE支持PBKDF2,这是一个用于口令散列的好算法。

1
2
3
4
5
6
7
8
byte[] salt=new byte[16];
random.nextBytes(salt);
KeySpec spec = new PBEKeySpec("password".toCharArray(), salt, 65536, 128);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = f.generateSecret(spec).getEncoded();
Base64.Encoder enc = Base64.getEncoder();
System.out.printf("salt: %s%n", enc.encodeToString(salt));
System.out.printf("hash: %s%n", enc.encodeToString(hash));

下面是一个实用程序类,您可以使用它进行pbkdf2密码验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

/**
 * Hash passwords for storage, and test passwords against password tokens.
 *
 * Instances of this class can be used concurrently by multiple threads.
 *  
 * @author erickson
 * @see StackOverflow
 */

public final class PasswordAuthentication
{

  /**
   * Each token produced by this class uses this identifier as a prefix.
   */

  public static final String ID ="$31$";

  /**
   * The minimum recommended cost, used by default
   */

  public static final int DEFAULT_COST = 16;

  private static final String ALGORITHM ="PBKDF2WithHmacSHA1";

  private static final int SIZE = 128;

  private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");

  private final SecureRandom random;

  private final int cost;

  public PasswordAuthentication()
  {
    this(DEFAULT_COST);
  }

  /**
   * Create a password manager with a specified cost
   *
   * @param cost the exponential computational cost of hashing a password, 0 to 30
   */

  public PasswordAuthentication(int cost)
  {
    iterations(cost); /* Validate cost */
    this.cost = cost;
    this.random = new SecureRandom();
  }

  private static int iterations(int cost)
  {
    if ((cost < 0) || (cost > 30))
      throw new IllegalArgumentException("cost:" + cost);
    return 1 << cost;
  }

  /**
   * Hash a password for storage.
   *
   * @return a secure authentication token to be stored for later authentication
   */

  public String hash(char[] password)
  {
    byte[] salt=new byte[SIZE / 8];
    random.nextBytes(salt);
    byte[] dk = pbkdf2(password, salt, 1 << cost);
    byte[] hash = new byte[salt.length + dk.length];
    System.arraycopy(salt, 0, hash, 0, salt.length);
    System.arraycopy(dk, 0, hash, salt.length, dk.length);
    Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
    return ID + cost + '$' + enc.encodeToString(hash);
  }

  /**
   * Authenticate with a password and a stored password token.
   *
   * @return true if the password and token match
   */

  public boolean authenticate(char[] password, String token)
  {
    Matcher m = layout.matcher(token);
    if (!m.matches())
      throw new IllegalArgumentException("Invalid token format");
    int iterations = iterations(Integer.parseInt(m.group(1)));
    byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
    byte[] salt=Arrays.copyOfRange(hash, 0, SIZE / 8);
    byte[] check = pbkdf2(password, salt, iterations);
    int zero = 0;
    for (int idx = 0; idx < check.length; ++idx)
      zero |= hash[salt.length + idx] ^ check[idx];
    return zero == 0;
  }

  private static byte[] pbkdf2(char[] password, byte[] salt, int iterations)
  {
    KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
    try {
      SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
      return f.generateSecret(spec).getEncoded();
    }
    catch (NoSuchAlgorithmException ex) {
      throw new IllegalStateException("Missing algorithm:" + ALGORITHM, ex);
    }
    catch (InvalidKeySpecException ex) {
      throw new IllegalStateException("Invalid SecretKeyFactory", ex);
    }
  }

  /**
   * Hash a password in an immutable {@code String}.
   *
   * <p>
Passwords should be stored in a {@code char[]} so that it can be filled
   * with zeros after use instead of lingering on the heap and elsewhere.
   *
   * @deprecated Use {@link #hash(char[])} instead
   */

  @Deprecated
  public String hash(String password)
  {
    return hash(password.toCharArray());
  }

  /**
   * Authenticate with a password in an immutable {@code String} and a stored
   * password token.
   *
   * @deprecated Use {@link #authenticate(char[],String)} instead.
   * @see #hash(String)
   */

  @Deprecated
  public boolean authenticate(String password, String token)
  {
    return authenticate(password.toCharArray(), token);
  }

}


下面是一个完整的实现,有两种方法可以完全满足您的需要:

1
2
String getSaltedHash(String password)
boolean checkPassword(String password, String stored)

关键是,即使攻击者能够访问您的数据库和源代码,密码仍然是安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base64;

public class Password {
    // The higher the number of iterations the more
    // expensive computing the hash is for us and
    // also for an attacker.
    private static final int iterations = 20*1000;
    private static final int saltLen = 32;
    private static final int desiredKeyLen = 256;

    /** Computes a salted PBKDF2 hash of given plaintext password
        suitable for storing in a database.
        Empty passwords are not supported. */

    public static String getSaltedHash(String password) throws Exception {
        byte[] salt=SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen);
        // store the salt with the password
        return Base64.encodeBase64String(salt) +"$" + hash(password, salt);
    }

    /** Checks whether given plaintext password corresponds
        to a stored salted hash of the password. */

    public static boolean check(String password, String stored) throws Exception{
        String[] saltAndHash = stored.split("\\$");
        if (saltAndHash.length != 2) {
            throw new IllegalStateException(
               "The stored password must have the form 'salt$hash'");
        }
        String hashOfInput = hash(password, Base64.decodeBase64(saltAndHash[0]));
        return hashOfInput.equals(saltAndHash[1]);
    }

    // using PBKDF2 from Sun, an alternative is https://github.com/wg/scrypt
    // cf. http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html
    private static String hash(String password, byte[] salt) throws Exception {
        if (password == null || password.length() == 0)
            throw new IllegalArgumentException("Empty passwords are not supported.");
        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        SecretKey key = f.generateSecret(new PBEKeySpec(
            password.toCharArray(), salt, iterations, desiredKeyLen));
        return Base64.encodeBase64String(key.getEncoded());
    }
}

我们正在存储'salt$iterated_hash(password, salt)'。salt是32个随机字节,其目的是如果两个不同的人选择相同的密码,存储的密码看起来仍然不同。

iterated_hash,基本上是hash(hash(hash(... hash(password, salt) ...))),对于有权访问数据库猜测密码、散列密码和在数据库中查找散列值的潜在攻击者来说,这是非常昂贵的。每次用户登录时,您都必须计算这个iterated_hash,但与攻击者花费几乎100%的时间计算散列相比,这不会花费您太多的成本。


BCRIPT是一个很好的库,它有一个Java端口。


您可以使用MessageDigest计算散列,但在安全性方面这是错误的。哈希不能用于存储密码,因为它们很容易被破坏。

您应该使用另一种算法,如bcrypt、pbkdf2和scrypt来存储密码。看这里。


除了其他答案中提到的bcrypt和pbkdf2,我建议您查看scrypt。

不建议使用MD5和SHA-1,因为它们速度相对较快,因此使用"每小时租金"分布式计算(例如EC2)或现代高端GPU,可以在相对较低的成本和合理的时间内使用暴力/字典攻击"破解"密码。

如果您必须使用它们,那么至少迭代算法一个预先定义的大量时间(1000以上)。

  • 请参阅此处了解更多信息:https://security.stackexchange.com/questions/211/how-to-security-hash-passwords

  • 这里是:http://codahale.com/how-to-safety-store-a-password/(出于密码散列的目的批评sha家族、md5等)

  • 这里是:http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html(批评bcrypt,建议scrypt和pbkdf2)

完全同意埃里克森的观点,即PBKdf2是答案。

如果您没有这个选项,或者只需要使用哈希,那么ApacheCommons DigestUtils比正确获取JCE代码容易得多:https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/digest/digestutils.html

如果使用哈希,请使用sha256或sha512。此页面对密码处理和散列有很好的建议(注意,它不建议对密码处理进行散列):http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html


您可以使用Shiro库(以前称为JSecurity)实现OWASP所描述的内容。

看起来Jasipt库也有类似的实用程序。


虽然已经提到了NIST的建议pbkdf2,但我想指出,2013年至2015年间,有一项公开密码散列竞争。最后,选择argon2作为推荐的密码散列函数。

对于可以使用的原生(原生C)库,有一个相当好的Java绑定。

在一般的用例中,我认为从安全的角度来看,如果您选择pbkdf2而不是argon2,那么这并不重要,反之亦然。如果您有很强的安全要求,我建议您在评估中考虑argon2。

有关密码散列函数安全性的更多信息,请参阅security.se。


这里有两个MD5哈希和其他哈希方法的链接:

JavaDocAPI:http://java.sun.com/j2se/1.4.2/docs/api/java/security/messagedigest.html

教程:http://www.twmacinta.com/myjava/fast_md5.php


您可以使用Spring安全加密(只有2个可选的编译依赖项),它支持pbkdf2、bcrypt和scrypt密码加密。

1
2
3
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder();
String sCryptedPassword = sCryptPasswordEncoder.encode("password");
boolean passwordIsValid = sCryptPasswordEncoder.matches("password", sCryptedPassword);
1
2
3
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String bCryptedPassword = bCryptPasswordEncoder.encode("password");
boolean passwordIsValid = bCryptPasswordEncoder.matches("password", bCryptedPassword);

1
2
3
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
String pbkdf2CryptedPassword = pbkdf2PasswordEncoder.encode("password");
boolean passwordIsValid = pbkdf2PasswordEncoder.matches("password", pbkdf2CryptedPassword);

在所有的标准哈希方案中,ldap ssha是最安全的一种,

http://www.openldap.org/faq/data/cache/347.html

我只需要按照这里指定的算法,并使用messagedigest进行散列。

您需要按照建议将盐存储在数据库中。