## 寫在前面
對于數據庫中如何保存密碼的認識過程。從最簡單的明文保存到密碼加鹽保存,下面與大家分享下:
## 第一階段
最開始接觸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。