本文轉載自【微信公眾號:五角錢的程式設計師,ID:xianglin965】經微信公眾號授權轉載,如需轉載與原文作者聯繫
一起學習、成長、溫情的熱愛生活
1.編碼算法
什麼是編碼?
ASCII碼
就是一種編碼,
字母A
的編碼是
十六進位的0x41
,字母B是
0x42
,以此類推:
因為ASCII編碼最多只能有127個字符,要想對更多的文字進行編碼,就需要用
Unicode
。而中文的
中
使用Unicode編碼就是
0x4e2d
,使用
UTF-8
則需要
3個字節
編碼:
因此,最簡單的編碼是直接給
每個字符
指定一個
若干字節表示的整數
,複雜一點的編碼就需要
根據一個已有的編碼推算出來
。
比如: UTF-8編碼,它是一種不定長編碼,但可以從給定字符的Unicode編碼推算出來。1.1 URL編碼
1.什麼是URL編碼算法
URL編碼是
瀏覽器
發送數據給伺服器時使用的編碼,它通常拼接在·URL的參數部分
例如:https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87之所以需要URL編碼,是因為出於
兼容性考慮
,很多伺服器
只識別ASCII字符
。但如果URL中
包含中文、日文
這些非ASCII字符怎麼辦?不要緊,URL編碼有一套規則:
如果字符是A~Z,a~z,0~9以及-、_、.、*,則保持不變;如果是其他字符,先轉換為UTF-8編碼,然後對每個字節以%XX表示。例如:字符"中"的UTF-8編碼是0xe4b8ad,因此,它的URL編碼是%E4%B8%AD。URL編碼總是大寫。2.Java中使用URL編碼算法
Java標準庫提供了一個
URLEncoder
類來對任意字符串進行URL編碼
import java.io.UnsupportedEncodingException;import java.net.URLEncoder;public class TestURLEncode {public static void main(String[] args) throws UnsupportedEncodingException { String encoded = URLEncoder.encode("中文!","UTF-8"); System.out.println(encoded); }}
image
上述代碼的運行結果是
%E4%B8%AD%E6%96%87%21
,中的URL編碼是
%E4%B8%AD
,文的URL編碼是
%E6%96%87
,
!
雖然是
ASCII
字符,也要對其編碼為
%21
。
和標準的URL編碼稍有不同,URLEncoder把空格字符編碼成「+」,而現在的URL編碼標準要求空格被編碼為「%20」,不過,伺服器都可以處理這兩種情況。!!!要特別注意:URL編碼是`編碼算法`,`不是加密算法`。URL編碼的目的是`把任意文本數據編碼為%前綴表示的文本`,編碼後的文本僅包含`A~Z,a~z,0~9,-,_,.,和%,便於瀏覽器和伺服器處理。</li> </ul> 如果伺服器收到URL編碼的字符串,就可以對其進行解碼,還原成原始字符串。Java標準庫的URLDecoder`就可以解碼:
import java.io.UnsupportedEncodingException;import java.net.URLDecoder;public class TestURLDecoder {public static void main(String[] args) throws UnsupportedEncodingException { String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", "UTF-8"); System.out.println(decoded); }}
1.2 Base64編碼
1.什麼是Base64編碼算法
URL編碼是對字符進行編碼,表示成%xx的形式而Base64編碼是對二進位數據進行編碼,表示成文本格式。Base64編碼可以把任意長度的二進位數據變為純文本,且只包含A~Z、a~z、0~9、+、/、=這些字符。它的原理是把3位元組的二進位數據按6bit(1個字節=2bit)一組,用4個int整數表示,然後查表,把int整數用索引對應到字符,得到編碼後的字符串
舉個例子:
3個byte
數據分別是
e4、b8、ad
,按
6bit分組
得到
39、0b、22和2d
:
因為
6位整數
的範圍總是
0~63
,所以,能用
64個字符
表示:字符
A~Z
對應索引
0~25
,字符
a~z
對應索引
26~51
,字符
0~9
對應索引
52~61
,最後兩個索引
62、63
分別用字符
+和/
表示。
2.Java中使用Base64編碼算法
在Java中,二進位數據就是
byte[]數組
。Java標準庫提供了
Base64
來對
byte[]數組
進行
編解碼
:
import java.util.Arrays;import java.util.Base64;public class TestBase64 {public static void main(String[] args) { //編碼 byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad }; String b64encoded = Base64.getEncoder().encodeToString(input); System.out.println(b64encoded); System.out.println("-----------------------"); //編碼後得到5Lit4個字符。要對Base64解碼,仍然用Base64這個類進行解碼 byte[] output = Base64.getDecoder().decode("5Lit"); System.out.println(Arrays.toString(output)); // [-28, -72, -83] }}
如果輸入的byte[]數組長度不是3的整數倍腫麼辦?
這種情況下,需要對輸入的末尾補一個或兩個0x00編碼後,在結尾加一個=表示補充了1個0x00,加兩個=表示補充了2個0x00解碼的時候,去掉末尾補充的一個或兩個0x00即可實際上,因為
編碼後的長度加上=總是4的倍數
,所以
即使不加=也可以計算出原始輸入的byte[]
。
Base64編碼的時候可以用withoutPadding()去掉=,解碼出來的結果是一樣的:
import java.util.Arrays;import java.util.Base64;public class TestBase642 {public static void main(String[] args) { byte[] input = new byte[]{(byte) 0xe4, (byte) 0xb8, (byte) 0xad, 0x21}; String b64encoded = Base64.getEncoder().encodeToString(input); System.out.println("encodeToString=>"+b64encoded); //用withoutPadding()去掉= String b64encoded2 = Base64.getEncoder().withoutPadding().encodeToString(input); System.out.println("withoutPadding=>"+b64encoded2); //將去掉=的Base64編碼重新解碼 byte[] output = Base64.getDecoder().decode(b64encoded2); System.out.println(Arrays.toString(output)); }}
因為標準的Base64編碼會出現
+、/和=
,所以不適合把Base64編碼後的字符串放到URL中。
一種針對URL的Base64編碼可以在URL中使用的Base64編碼,它僅僅是把+變成-,/變成_
import java.util.Arrays;import java.util.Base64;public class TestBase64URL {public static void main(String[] args) { //編碼 byte[] input = new byte[]{0x01, 0x02, 0x7f, 0x00}; String b64encoded = Base64.getUrlEncoder().encodeToString(input); System.out.println(b64encoded); //解碼 byte[] output = Base64.getUrlDecoder().decode(b64encoded); System.out.println(Arrays.toString(output)); }}
Base64編碼的目的是把二進位數據變成文本格式,這樣在很多文本中就可以處理二進位數據。例如,電子郵件協議就是文本協議,如果要在電子郵件中添加一個二進位文件,就可以用Base64編碼,然後以文本的形式傳送。Base64編碼的缺點是:傳輸效率會降低,因為它把原始數據的長度增加了1/3。和URL編碼一樣,Base64編碼是一種編碼算法,不是加密算法。如果把Base64的64個字符編碼表換成32個、48個或者58個,就可以使用Base32編碼,Base48編碼和Base58編碼。字符越少,編碼的效率就會越低。小結
URL編碼和Base64編碼都是編碼算法,它們不是加密算法;URL編碼的目的是把任意文本數據編碼為"%"前綴表示的文本,便於瀏覽器和伺服器處理;Base64編碼的目的是把任意二進位數據編碼為文本,但編碼後數據量會增加1/3,傳輸效率會降低。
2. 什麼是哈希算法?
哈希算法(Hash)又稱
摘要算法(Digest)
,它的作用是:對任意一組輸入數據進行計算,得到一個固定長度的輸出摘要。
哈希算法最重要的特點就是:
相同的輸入一定得到相同的輸出;不同的輸入大概率得到不同的輸出。哈希算法的目的就是為了
驗證原始數據是否被篡改
。
Java字符串的
hashCode()
方法就是一個哈希算法,它的
輸入
是任意字符串,
輸出
是固定的
4位元組int整數:
System.out.println("hello".hashCode()); // 99162322System.out.println("hello, java".hashCode()); // 2057144552System.out.println("hello, bob".hashCode()); // -1596215761
兩個相同的字符串永遠會計算出相同的hashCode
,否則基於
hashCode
定位的HashMap就無法正常工作。這也是為什麼當我們自定義一個class時,
覆寫equals()方法時我們必須正確覆寫hashCode()方法。
2.1 哈希碰撞
哈希碰撞是指:
兩個不同的輸入得到了相同的輸出:
System.out.println("AaAaAa".hashCode());; // 1952508096System.out.println("BBAaBB".hashCode());// 1952508096
碰撞能不能避免?答案是不能。
碰撞是一定會出現的
,因為
輸出的字節長度是固定的
String的hashCode()輸出是4位元組整數,最多只有4294967296種輸出,但輸入的數據長度是不固定的,有無數種輸入。所以,哈希算法是把一個無限的輸入集合映射到一個有限的輸出集合,必然會產生碰撞。碰撞不可怕,我們擔心的不是碰撞,而是
碰撞的概率
,因為
碰撞概率的高低關係
到哈希算法的
安全性
。一個安全的哈希算法必須滿足:
碰撞概率低;不能猜測輸出。**不能猜測輸出是指 : **輸入的任意一個bit的變化會造成輸出完全不同,這樣就很難從輸出反推輸入(只能依靠暴力窮舉)。假設一種哈希算法有如下規律:
hashA("java001") = "123456"hashA("java002") = "123457"hashA("java003") = "123458"
那麼很容易從輸出123459反推輸入,這種哈希算法就不安全。安全的哈希算法從輸出是看不出任何規律的
hashB("java001") = "123456"hashB("java002") = "580271"hashB("java003") = ???
2.2 常用的哈希算法
根據碰撞概率,
哈希算法的輸出長度越長
,就
越難產生碰撞
,也就
越安全
。
MD5 加密後的位數有兩種:16 位與 32 位。16 位實際上是從 32 位字符串中取中間的第 9 位到第 24 位的部分,用 Java 語言來說,即:String md5_16 = md5_32.substring(8, 24);
MD5 加密後的字符串又分為
大寫與小寫兩種
,也就是
其中的字母是大寫還是小寫
。所以對字符串「yjclsx」進行 MD5 加密後的結果類型有這些:
Java 中 MD5 加密的結果默認是32位小寫。
2.3 Java中使用哈希算法
Java標準庫提供了
常用的哈希算法,並且有一套統一的接口
。我們以
MD5算法為例
,看看如何對輸入計算哈希:
import java.math.BigInteger;import java.security.MessageDigest;public class TestMD5 {public static void main(String[] args) throws Exception { // 創建一個MessageDigest實例: MessageDigest md = MessageDigest.getInstance("MD5"); // 反覆調用update輸入數據: md.update("Hello".getBytes("UTF-8")); md.update("World".getBytes("UTF-8")); byte[] result = md.digest(); // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6 //轉換為十六進位的字符串 System.out.println(new BigInteger(1, result).toString(16)); }}
image
使用MessageDigest時,我們首先根據哈希算法獲取一個
MessageDigest實例
,然後,反覆
調用update(byte[])輸入數據
。當輸入結束後,調用
digest()方法獲得byte[]數組表示的摘要
,最後,把它轉換為十六進位的字符串。
2.4 哈希算法的用途
因為相同的輸入永遠會得到相同的輸出,因此,如果輸入被修改了,得到的輸出就會不同。
我們在網站上下載軟體的時候,經常看到下載頁顯示的哈希:
如何判斷下載到本地的軟體是原始的、未經篡改的文件?
我們只需要自己計算一下本地文件的哈希值,再與官網公開的哈希值對比,如果相同,說明文件下載正確,否則,說明文件已被篡改。哈希算法的另一個重要用途是存儲用戶密碼。如果直接將用戶的
原始密碼存放到資料庫中
,會產生極大的安全風險:
資料庫管理員能夠看到用戶明文密碼;資料庫數據一旦洩漏,黑客即可獲取用戶明文密碼。不存儲用戶的原始口令,那麼如何對用戶進行認證?
方法是存儲用戶密碼的哈希,例如:MD5。在用戶輸入原始密碼後,系統計算用戶輸入的原始密碼的MD5並與資料庫存儲的MD5對比,如果一致,說明密碼正確,否則,密碼錯誤。因此資料庫存儲用戶名和密碼的表內容應該像下面這樣:
這樣一來,
資料庫管理員看不到用戶的原始密碼
。即使資料庫洩漏,黑客也無法拿到用戶的原始密碼。想要拿到用戶的原始密碼,必須用
暴力窮舉
的方法,
一個密碼一個密碼地試,直到某個密碼計算的MD5恰好等於指定值
。
使用哈希密碼時,還要注意防止彩虹表攻擊。
什麼是彩虹表呢?上面講到了,如果只拿到MD5,從MD5反推明文密碼,只能使用暴力窮舉的方法。然而黑客並不笨,暴力窮舉會消耗大量的算力和時間。但是,如果有一個預先計算好的常用密碼和它們的MD5的對照表:這個表就是彩虹表。如果用戶使用了
常用密碼
,黑客從MD5一下就能反查到原始密碼
bob的MD5:
f30aa7a662c728b7407c54ae6bfd27d1
,
原始密碼:
hello123
;
alice的MD5:
25d55ad283aa400af464c76d713c07ad
,
原始密碼:
12345678
;
tim的MD5:
bed128365216c019988915ed3add75fb
,
原始秘密:
passw0rd
這就是為什麼不要使用常用密碼,以及不要使用生日作為密碼的原因。
加鹽
即使用戶使用了常用密碼,我們也可以採取措施來
抵禦彩虹表攻擊
,方法是對
每個密碼額外添加隨機數
,這個方法稱之為
加鹽(salt)
:
digest = md5(salt+inputPassword)
經過加鹽處理的資料庫表,內容如下:
image
加鹽的目的在於
使黑客的彩虹表失效,即使用戶使用常用密碼,也無法從MD5反推原始密碼。
2.5 .SHA-1
SHA-1也是一種哈希算法,它的輸出是
160 bits,即20位元組
。
SHA-1是由美國國家安全局開發的,SHA算法實際上是一個系列,包括SHA-0(已廢棄)、SHA-1、SHA-256、SHA-512等。在Java中使用SHA-1,和MD5完全一樣,只需要把算法名稱改為
"SHA-1"
:
import java.math.BigInteger;import java.security.MessageDigest;public class TestSHA1 {public static void main(String[] args) throws Exception { // 創建一個MessageDigest實例: MessageDigest md = MessageDigest.getInstance("SHA-1"); // 反覆調用update輸入數據: md.update("Hello".getBytes("UTF-8")); md.update("World".getBytes("UTF-8")); byte[] result = md.digest(); // 20 bytes: 6f44e49f848dd8ed27f73f59ab5bd4631b3f6b0d //轉為16進位字符串 System.out.println(new BigInteger(1, result).toString(16)); }}
image
小結
哈希算法可用於驗證數據完整性,具有防篡改檢測的功能;常用的哈希算法有MD5、SHA-1等;用哈希存儲口令時要考慮彩虹表攻擊。