保 证用户账号密码的安全无需过多解释,如果用户密码被泄露或者破解会,曾经某个网站,因为一个漏洞,整站数据库泄露,所有的账号密码都暴露出来,而且很多人习惯使用相同的用户名密码,一套账号密码可能在多个网站上使用,如果包含果能通过网银,支付宝等账号,那问题就非常严重了。本文介绍如何更安全地保存用户密码,提供一个案例:
根本原则就是在时间复杂度和空间复杂度上增加破解的难度。
一个原则是尽可能确保密码的安全性。有一下几个原则:
- 一定不要保存密码明文:一方面可能会由于系统漏洞造成数据泄露,其严重性不言自明。另一方面,家贼难防,用户账号也能被内鬼泄露出去。
- 使用单项不可逆加密算法:讲用户密码进行信息摘要计算,系统只保存摘要,不保存密码明文。认证时,对用户输入的密码再次进行摘要计算,在和数据库保存的信息摘要进行比对,由于使用不可逆的摘要算法,无法通过摘要反解出原。目前,MD5算法和SHA1 摘要算法已经被攻破,不再安全,推荐使用SHA-256以上的摘要算法。
- 给摘要加点“盐(salt)” 用来增强摘要算法的安全性。由于常用的摘要算法基本都是公开通用算法,谁都可以使用,只使用摘要算法,经过长时间的计算和积累,攻击者会积累一定规模的摘要,摘要对照表(也被称做彩虹表),一旦摘要泄露,尤其是使用弱密码的摘要,就可以通过对照表反差出密码原文。所以需要再再计算摘要过程前,使用一个额外的数据追加到密码上,以便增加密码强度,一同计算摘要。
比如使用MD5计算的0~9,a~z(包括大写)等62个字符计算的彩虹表,规模高达 13,759,005,997,841,642 个,存储达到864GB(http://project-rainbowcrack.com/table.htm),这还没有包含各种符号,再此基础之上没郑家一个符号都是指数级的计算量的增长,代价是巨大的。
这样就增加了密码破解难度,这段额外信息被称为“盐(salt)”,盐和信息摘要同时存储。目前主要有两种加盐方式,盐的长度比低于64位(在RFC2898文案中推荐):
- 固定盐:固定盐就是使用输定数据参与摘要计算,好处是实现简单,缺点是安全性不足,一旦“盐”被泄露会影响所有用户。
随机盐:每次计算摘要计算都使用的盐,比如按照一定的规则,针对每一个用户生成一个随机盐,一个用户使用同一个盐,不同之间的用户“盐”不用,这样即使一个用户密码被破解,也不影响其他用户,另外在用户重置密码时也需要重新生成盐。
循环hash,延长计算时间,增加破解难度,PBKDF2, bcrypt, scrypt 就是这类的算饭。由于 MD5,SHA系列算法都是运算比较快的算法,随着硬件的发展,单次摘要计算获得结果也变得容易被破解,可以说只是时间问题,为了应对这种问题,引入了“拉伸”概念,就是通过多次循环 hash 加密算法变慢((在RFC2898文案中推荐 推荐不小于1000次),增加攻击者的时间成本,使得暴力破解变得非常困难。
(TODO 补图)
- 使用HTTPS传输用户密码:
package ok;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.apache.commons.codec.binary.Base64;
public class UserPasswordHash {
// 生成摘要长度 512 位,理论上越长的摘要越难破解。
private static final int HASH_BIT_SIZE = 512;
// 迭代次数,按照 在RFC2898文案中推荐 的建议,不少以100次
private static final int ITERATIONS = 2000;
// 盐的长度,按照 RFC2898 中的建议,盐的长度不低于64位
private static final int SALT_BIT_SIZE = 64;
// 创建密码摘要
public static String genPasswordHash(String password, String salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), Base64.decodeBase64(salt), ITERATIONS, HASH_BIT_SIZE);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
byte[] hash = skf.generateSecret(spec).getEncoded();
return Base64.encodeBase64String(hash);
}
// 生成随机盐
public static String genRandomSalt() {
byte[] salt = new byte[SALT_BIT_SIZE];
SecureRandom rand = new SecureRandom();
rand.nextBytes(salt);
return Base64.encodeBase64String(salt);
}
// 验证密码
public static boolean verify(String password, String salt, String passHash)
throws NoSuchAlgorithmException, InvalidKeySpecException {
String hash = genPasswordHash(password, salt);
return hash.equals(passHash);
}
public static void savePasswordDemo(String passwordHash, String salt) {
// TODO 讲密码Hash 和 salt 同时存储
}
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
//原始密码
String weakPassword = "123456";
//生成随机盐
String salt = genRandomSalt();
//经过加盐后的密码摘要
String passwordHash = genPasswordHash(weakPassword, salt);
//同时储存密码hash和盐
savePasswordDemo(passwordHash, salt);
//验证密码
boolean resualt = verify(weakPassword, salt, passwordHash);
System.out.println(resualt);
}
}