很多人都用MD5+Base64方式存儲密碼,這種存儲方式 方便、速度快而且由於MD5雜湊算法的幾乎不可還原性,攻擊者只能通過"猜"去破解密碼。
但是MD5對相同的數據返回的信息永遠是一樣的,"123456"通過MD5+Base64編碼後,永遠是"4QrcOUm6Wau+VuBX8g+IPg==",攻擊者只需要一個簡單的sql語句:select * from userInfo where password='4QrcOUm6Wau+VuBX8g+IPg==' 就可以知道有幾個用戶密碼是"123456",這對一個項目來說十分危險。
//這個循環無論執行幾次,返回都是一樣。
static void t3(){
for(int i=0;i<5;i++){
try {
String inputPwd = "123456";
MessageDigest md;
BASE64Encoder b64Encoder = new BASE64Encoder();
byte[] bys = null;
md = MessageDigest.getInstance("MD5","SUN");
md.reset();
md.update(inputPwd.getBytes("UTF-8"));
bys = md.digest();
System.out.println("編碼後:"+b64Encoder.encode(bys));
} catch (Exception e) {
e.printStackTrace();
}
}
}
一種簡單有效的解決方法,就是在MD5雜湊前加點隨機碼,這樣攻擊者將無法通過一個簡單的SQL語句來查找密碼是"123456"的用戶。
隨機碼長度是應該固定,且同用戶輸入的密碼一起進行MD5雜湊,隨機碼應該在Base64編碼後的固定位置。
校驗時,先取出隨機碼,再同輸入一起進行MD5雜湊,與以前存儲的密碼比較是否一致。
package test1;
import java.security.MessageDigest;
import java.util.Random;
import sun.misc.BASE64Encoder;
/**
* 對帳號口令加密
* @author shanl
*
*/
public class PasswdEncryption {
private static final MessageDigest md;
private static final BASE64Encoder b64Encoder;
static{
try {
md = MessageDigest.getInstance("MD5", "SUN");
b64Encoder = new BASE64Encoder();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 檢查密碼
* @param inputPasswd 用戶輸入的密碼
* @param storePasswd 已存儲的密碼
* @return true:通過檢查,false:未通過
*/
synchronized
public static boolean checkPasswd(String inputPasswd, String storePasswd){
boolean ok = false;
try{
byte[] saltBys = storePasswd.substring(0,2).getBytes("UTF-8");
String inPwd = toPasswd(inputPasswd, saltBys);
ok = inPwd.equals(storePasswd);
}catch(Exception ex){
ex.printStackTrace();
ok = false;
}
return ok;
}
/**
* 將客戶輸入的密碼加密
* @param inputPasswd
* @return
*/
synchronized
public static String toPasswd(String inputPasswd){
byte[] salt = getSaltOfASCII(2);
return toPasswd(inputPasswd, salt);
}
/**
* 將客戶輸入的密碼加密
* @param inputPasswd 客戶輸入的密碼
* @param salt 鹽
* @return 加密後的字符串
*/
synchronized
private static String toPasswd(String inputPasswd, byte[] salt){
String pwd = "";
try{
md.reset();
md.update(salt);
md.update(inputPasswd.getBytes("UTF-8"));
byte[] bys = md.digest();;
//1:AASLexNtFtI7e1IuQIg88ZNA==
//879:AA/lCM5NEwVQJ25YYomE1ldQ==
//1000:AARXKQat7z+/iu2w6KpKgLQA==
// for(int i=0; i<1000; i++){
// md.reset();
// bys = md.digest(bys);
// }
pwd += (char)salt[0];
pwd += (char)salt[1];
pwd += b64Encoder.encode(bys);
}catch(Exception ex){
ex.printStackTrace();
pwd = "";
}
return pwd;
}
/**
* 返回指定長度的鹽(ASCII碼)
* @param len 長度
* @return
*/
private static byte[] getSaltOfASCII(int len){
byte[] salt = new byte[len];
Random rand = new Random();
for(int i=0; i<len; i++){
// salt[i] = 'A';
salt[i] = (byte) ((rand.nextInt('~'-'!')+'!') & 0x007f);
}
return salt;
}
}
測試類:
package test1;
public class Test8 {
public static void main(String[] args){
t2();
// t1();
}
static void t2(){
String[] userPwds = {
"123456",
"123456",
"123456",
"123456",
"123456",
"1234561", //error input
};
//通過t1()得到
String[] dbPwds = {
"u:rkoqEBZ+IZlcugSyVkhNVg==",
"vDEbVZ9JlSveGnB/Qdz69aAQ==",
"hbgon/TjwH8s7YSuqVSDl9Yg==",
"0-ExML8oTOSCeF3ETf1yvDHg==",
"1TeHpWiXOhx5ilscC6D6G98g==",
"1TeHpWiXOhx5ilscC6D6G98g==",
};
String msg = "";
for(int i=0,end=dbPwds.length; i<end; i++){
msg = "user input passwd:"+userPwds[i];
msg += ",db store passwd:"+dbPwds[i];
msg += ",check passwd:"+PasswdEncryption.checkPasswd(userPwds[i],dbPwds[i]);
System.out.println(msg);
}
}
/**
* 加密測試
*/
static void t1(){
String pwd = "123456";
String pwd1 = null;//PasswdEncryption.toPasswd(pwd);
for(int i=0; i<5; i++){
pwd1 = PasswdEncryption.toPasswd(pwd);
System.out.println(pwd1);
}
}
}
t1()結果顯示,在很小的情況下會出現相同的加密後密碼,攻擊者很難用一個簡單SQL去查找同一密碼的用戶。
t2()則可以用同一個密碼"123456"驗證,多個加密後的密碼。