## 写在前面
对于数据库中如何保存密码的认识过程。从最简单的明文保存到密码加盐保存,下面与大家分享下:
## 第一阶段
最开始接触web开发时,对于用户表的密码基本是明文保存,如:
~~~
username | password
---------|----------
zp1996 |123456
zpy |123456789
~~~
这种方式可以说很不安全,一旦数据库泄漏,那么所以得用户信息就会被泄漏。之前,国内普遍采用这种方式,造成了很多的事故,如csdn600万用户信息泄漏、12306用户信息泄漏等。
## 第二阶段
本人大学做过的所有的项目基本采用的都是这种方式来保存用户密码,就是对密码进行md5加密,在php中md5即可,在node中利用crypto模块就好:
~~~
const encrypt = (text) => {
return crypto.createHash("md5").update(String(text)).digest("hex");
};
~~~
作为初学者的我,认为这种方式是很安全的,因为md5不可逆(指攻击者不能从哈希值h(x)中逆推出x)而且碰撞几率低(指攻击值不能找到两个值x、x’具有相同的哈希值);然而这种方式也是不安全的,只要枚举出所有的常用密码,做成一个索引表,就可以推出来原始密码,这张索引表也被叫做“彩虹表”(之前csdn600万用户明文密码就是一个很好的素材)。
## 第三阶段
这种方式是在实习中学习到的,也就是对密码来进行加盐。
## 什么是加盐?
在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
加盐很好理解,就是给原始密码加上特定的字符串,这样给攻击者增加攻击的成本,加盐的关键在于如何选择盐:
固定字符串
采用固定的字符串作为盐,如下面这样:
~~~
const encrypt = (text) => {
text = text + 'zp';
return crypto.createHash("md5").update(text).digest("hex");
};
~~~
这种加盐方式与多进行几次md5一样的,没有任何意义,攻击者都可以拿到数据库,难道拿不到源代码吗,根据源代码攻击者很轻松的就可以构造新的彩虹表出来逆推密码。
随机字符串
盐一般要求是固定长度的随机字符串,且每个用户的盐不同,比如10位,数据库可以这样存储:
~~~
username | password |salt
---------|---------—------------------------|----------
zp1996 |2636fd8789595482abf3423833901f6e |63UrCwJhTH
zpy |659ec972c3ed72d04fac7a2147b5827b |84GljVnhDT
~~~
采用的加密方式为:
`md5(md5(password) + salt)`
## 写在最后
以随机字符串作为盐对密码进行加盐仅仅是增加破解密码的难度,假如目前有30w的用户数据,那么就会有30w个盐,利用600w的索引表去比对的话,需要创造出30w * 600w的数据来一一比对,这样会增加攻击者的成本。
## 加密
我们在用户模块,对于用户密码的保护,通常都会进行加密。从最简单来说,小明盗取了你的数据库信息(小明躺枪),但由于你对你数据库中的用户信息的密码是加密的(我们假设加密之后的密文是无法破解的),那小明即使得到信息也没法进行登录。这是最最基本的一点防范措施。
我们通常的做法是,用户在提交注册信息时,在后台的业务逻辑中将密码进行加密(例如采用MD5或者BCrypt加密算法),所以存放在数据库中的信息为加密之后的密文。例如,如果小红在你的系统中注册了自己的账号,她提交的注册信息中的密码为”admin”,那么实际存到数据库中的密码为“21232F297A57A5A743894A0E4A801FC3”(假设采用MD5加密,并且不会被破解)。这样我们至少保证了只有小红本人能够通过其账号进行登录,因为密码只有她自己知道。
当小红用其账号进行登录的过程中,她将自己的用户名和密码提交给后台的服务器,服务器得到密码之后,采用同样的加密方法(MD5加密),也会得到密文,这个时候再与数据库中的密码字段的数据进行字符串的比较,相同就代表验证通过。
PS:顺便提一句,MD5和BCrypt加密算法都比较流行,相对来说,BCrypt算法比MD5更安全,但是加密更慢。
## 加盐
上文就是对于加密的一个简单陈述。那什么是加盐呢?当我第一次看到这个词的时候,我想到了我妈做的饭,因为我妈做饭一直都很淡= =
回到咱们要讲的加盐(Salt)。其实加盐是为了应对这么一种情况:如果两个人或多个人的密码相同,那么通过相同的加密算法得到的是相同的结果。这样会造成哪些后果呢?首先,破解一个就有可能是相当于破一片密码。而且加入小明这个用户可以查看后台数据库,那么如果他观察到小红这个用户的密码跟自己的密码是一样的(虽然都是密文),那么,也就代表他们两个人的密码是相同的。所以他就可以用小红的身份进行登录了。
其实,我们只要稍微混淆一下就能防范住了,这在加密术语中称为“加盐”。具体来说就是在原有材料(用户自定义密码)中加入其他成分(一般是用户自有且不变的因素),以此来增加系统复杂度。当这种盐和用户密码结合后,再通过摘要处理,就能得到隐蔽性更强的摘要值。
## 关于BCrypt加密的Demo
在本文中,将不去介绍如何实现MD5加密以及加盐,因为MD5太普遍。我记得在大一开始学Java的时候,每当遇到用户管理,对用户信息的加密都是通过MD5来进行的,但鉴于我自身的情况,我也是后期才晓得BCrypt这个加密算法,所以我也自己做了Demo来进行学习一下。
1、可以在官网中取得源代码http://www.mindrot.org/projects/jBCrypt/
2、通过Ant进行编译。编译之后得到jbcrypt.jar。也可以不需要进行编译,而直接使用源码中的java文件(本身仅一个文件)。
3、下面是官网的一个Demo
~~~
public class BCryptDemo {
public static void main(String[] args) {
// Hash a password for the first time
String password = "testpassword";
String hashed = BCrypt.hashpw(password, BCrypt.gensalt());
System.out.println(hashed);
// gensalt's log_rounds parameter determines the complexity
// the work factor is 2**log_rounds, and the default is 10
String hashed2 = BCrypt.hashpw(password, BCrypt.gensalt(12));
// Check that an unencrypted password matches one that has
// previously been hashed
String candidate = "testpassword";
//String candidate = "wrongtestpassword";
if (BCrypt.checkpw(candidate, hashed))
System.out.println("It matches");
else
System.out.println("It does not match");
}
}
~~~
在这个例子中,`BCrypt.hashpw(password, BCrypt.gensalt())`是核心。通过调用BCrypt类的静态方法hashpw对password进行加密。第二个参数就是我们平时所说的加盐。`BCrypt.checkpw(candidate, hashed)`该方法就是对用户后来输入的密码进行比较。如果能够匹配,返回true。