了不起的Base64
前言
在我們項目開發中,Base64想必大家都不會很陌生,Base64是將「二進制數據」轉換為文本的一種優雅方式,使存儲和傳輸變得容易。但是,作為一個合格的程序員,我們應該有一種打破砂鍋問到底的求助欲望。
所以,今天我們來講講在各種語言中出鏡率都高的離譜的Base64算法。今天,我們就用我們在初高中語文老師教我們的描述一個事物的三大步驟:1. 是什么,2. 如何工作,3. 為什么它很重要。來講講Base64算法。
好了,天不早了,干點正事哇。
我們能所學到的知識點
- 前置知識點
- 為什么會出現 Base64 編碼
- 什么是 Base64 編碼?
- Base64 使用案例
- Base64 編碼算法
- 如何進行 Base64 編碼和解碼
1. 前置知識點
「前置知識點」,只是做一個概念的介紹,不會做深度解釋。因為,這些概念在下面文章中會有出現,為了讓行文更加的順暢,所以將本該在文內的概念解釋放到前面來。「如果大家對這些概念熟悉,可以直接忽略」同時,由于閱讀我文章的群體有很多,所以有些知識點可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識點,請「酌情使用」。
RFC
RFC,全稱為Request for Comments,是一種用于定義「互聯網標準和協議」的文件系列。
RFC最早由互聯網工程任務組(IETF)創建,用于記錄和傳播互聯網協議、方法和最佳實踐的提案、規范和討論。
「每個 RFC 都有一個唯一的編號」,通常以RFC開頭,后面跟著一個數字,例如RFC 791、RFC 2616等。RFC文檔通常包含了協議規范、技術說明、最佳實踐、標準化提案等,以促進互聯網技術的發展和互操作性。
我們可以在IETF-datatracker[1]中輸入指定的編號或者查找的關鍵字進行搜尋。
圖片
以下是一些常見的RFC文檔,大家可以翻閱自己想了解的技術點:
- RFC 791 - Internet Protocol (IP): 定義了 IPv4,是互聯網上最基本的協議之一。
- RFC 793 - Transmission Control Protocol (TCP): 定義了 TCP,一種重要的傳輸協議,用于可靠的數據傳輸。
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1: 定義了 HTTP 協議,用于在 Web 上傳輸超文本的基礎通信協議。
- RFC 2326 - Real Time Streaming Protocol (RTSP): RTSP 用于流媒體傳輸,如音頻和視頻流的控制。
- RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: 定義了 TLS 1.2,用于安全地傳輸數據,如 HTTPS 協議中使用的加密通信。
- RFC 4648[2] - 這是咱們今天的主角,Base64的相關內容
Latin-1 字符集
Latin-1,也稱為ISO-8859-1,是一種由國際標準化組織(ISO)認可的「8 位字符集」,代表了「西歐語言的字母表」。正如其名稱所示,「它是ISO-8859的一個子集」,該標準還包括用于寫作系統如西里爾文、希伯來文和阿拉伯文的其他相關字符集。它被大多數Unix系統以及Windows系統使用。
Latin-1有時被不太準確地稱為「擴展 ASCII」。
這是因為其字符集的前 128 個字符與美國 ASCII 標準相同。其余字符集包含了帶重音的字符和符號。
關于更詳細的Latin-1的表格,可以參考Latin-1-table[3]
btoa
btoa 是 JavaScript 中的一個內置函數,用于將二進制數據(通常是 8 位字節)編碼為 Base64 字符串。它的名稱是 binary to ASCII 的縮寫,用于將二進制數據轉換為文本字符串,以便在文本協議中傳輸或存儲。
用法:
btoa 函數接受一個字符串參數,該字符串包含二進制數據。它將該二進制數據轉換為 Base64 編碼的字符串。
const binaryData = "front789";
const base64String = btoa(binaryData);
console.log(base64String);
這段代碼將 front789 這個字符串轉換為 Base64 編碼的字符串并將結果打印到控制臺。
限制:
盡管 btoa 是一個有用的函數,但它有一些限制:
- 「只能編碼字符串:」 btoa 函數只接受字符串作為參數,而不接受其他類型的數據(如二進制數組)。如果需要編碼二進制數據,需要先將其轉換為字符串。
- 「字符集限制:」 btoa 函數僅支持 Latin-1 字符集,這意味著它只能編碼包含在 Latin-1 字符集內的字符。如果字符串包含超出 Latin-1 字符集的字符,那么會導致編碼失敗。
- 「不適合加密:」Base64 編碼不是加密,它只是一種編碼方式,不提供安全性。如果需要加密數據,應該使用專門的加密算法而不是僅僅進行 Base64 編碼。
- 「數據大小增加:」 Base64 編碼會增加數據大小。通常情況下,Base64 編碼后的數據會比原始二進制數據更大,這可能會對數據傳輸和存儲造成額外開銷。
Data URL
Data URL 是一種統一資源標識符(URI)方案,用于將數據嵌入到文檔中,而不是從外部文件加載數據。Data URL 允許我們將數據(如文本、圖像、音頻等)直接包含在網頁或文檔中,而不需要額外的 HTTP 請求。這種方式對于小型資源或需要避免外部請求的情況非常有用。
Data URL 的基本結構如下:
data:[<mediatype>][;base64],<data>
其中:
- <mediatype> 是可選的媒體類型(例如,text/plain 或 image/png),用于描述數據的類型。如果被省略,則默認值為 text/plain;charset=US-ASCII。
- ;base64 是可選的,表示數據以 Base64 編碼方式包含。如果省略了 ;base64,則數據將以純文本方式包含。
- <data> 包含實際的數據,可以是文本或二進制數據。
以下是 Data URL 的一些常見用途和示例:
- 「嵌入圖像:」Data URL 可用于將圖像直接嵌入HTML或CSS中,而不需要外部圖像文件。例如,將一張 PNG 圖像嵌入 HTML 中:
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg=="
alt="Embedded Image"
/>
- 「內聯 CSS:」Data URL可用于內聯CSS樣式表,以減少外部CSS文件的請求。例如,將CSS樣式表嵌入 HTML 中:
<style>
body {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg==);
}
</style>
- 「嵌入字體:」Data URL可用于嵌入自定義字體,以確保字體在不同設備上顯示一致。例如,嵌入一個字體文件:
@font-face {
font-family: "CustomFont";
src: url(data:application/font-woff;base64,d09GRgABAAAA...) format("woff");
}
- 「內聯腳本:」Data URL可用于內聯小型JavaScript腳本,以減少外部腳本文件的請求。例如,內聯一個簡單的JavaScript函數:
<script>
let greeting = "前端柒八九";
alert(greeting);
</script>
2. 為什么會出現 Base64 編碼
要理解為什么需要 Base64 編碼,我們需要了解一些計算機歷史。
計算機以二進制(0 和 1)進行通信,但人們通常希望使用更豐富的數據形式進行通信,如文本或圖像。「為了在計算機之間傳輸數據,首先必須將其編碼為 0 和 1,然后再解碼」。以文本為例,有許多不同的編碼方式。如果我們都能就一個單一的編碼方式達成一致,那將會簡單得多,但很遺憾,這并不是事實。針對這塊的內容,可以參考了不起的 Unicode
最初創建了許多不同的編碼方式(例如 Baudot 編碼),每種方式「使用不同數量的比特來表示一個字符」,直到最終 ASCII 成為一個標準,「每個字符使用 7 位」。然而,大多數「計算機將二進制數據存儲為每個字節由 8 位組成的數據」,因此 ASCII 不適合傳輸這種類型的數據。一些系統甚至會刪除最高位。
為解決這些問題,引入了 Base64 編碼。這允許我們「將任意字節編碼為已知不會損壞的字節」(ASCII 字母數字字符和一些符號)。缺點是使用 Base64 對消息進行編碼會增加其長度 - 「每 3 個字節的數據編碼為 4 個 ASCII 字符」。
要可靠地發送文本,我們可以首先使用自己選擇的文本編碼(例如 UTF-8)將其編碼為字節,然后將結果的二進制數據使用 Base64 編碼為可安全傳輸的 ASCII 文本字符串。接收者反轉此過程以恢復原始消息。當然,這需要接收者知道使用了哪種編碼,通常需要單獨發送這些信息。
我們來看一個示例:
我希望發送一個帶有兩行的文本消息:
Hello
world!
如果我將其發送為 ASCII(或 UTF-8),它將如下所示:
72 101 108 108 111 10 119 111 114 108 100 33
某些系統會破壞字節 10,所以我們可以將這些字節作為 Base64 字符串進行 Base64 編碼:
SGVsbG8Kd29ybGQh
這里的所有字節都是已知的安全字節,所以很少有機會使任何系統損壞此消息。我可以發送這個消息而不是我的原始消息,然后讓接收者反轉此過程以恢復原始消息。
3. 什么是 Base64 編碼?
Base64編碼將二進制數據轉換為文本,具體來說是ASCII文本。生成的文本僅包含A-Z、a-z、0-9以及符號+和/這些字符。
而在之前我們在了不起的 Unicode中介紹過ASCII的。
由于字母表中有 26 個字母,我們有26 + 26 + 10 + 2(64)個字符。因此,這種編碼被命名為Base64。這 64 個字符被認為是「安全」的,也就是說,與字符<、>、\n等不同,「它們不會被舊計算機和程序誤解」。
下面是經過 Base64 編碼的文本front789的樣子:ZnJvbnQ3ODk=。
還有一點需要注意,如果在使用JS對某一個文本進行準換時,如果該文本包含非Latin1字符的字符串,會報錯,所以我們需要對其進行準換處理。
// 原始文本字符串,包含非Latin1字符
const text = "前端柒八九";
// 創建一個 TextEncoder 對象,用于將文本編碼為字節數組
const encoder = new TextEncoder();
// 使用 TextEncoder 對象將文本編碼為字節數組
const data = encoder.encode(text);
// 使用 String.fromCharCode 和展開運算符 (...) 將字節數組轉換為字符串
// 然后使用 btoa 函數將字符串轉換為 Base64 編碼
const base64 = btoa(String.fromCharCode(...data));
// 打印 Base64 編碼后的結果
console.log(base64); //5YmN56uv5p+S5YWr5Lmd
我們在這里并沒有加密文本。給定Base64編碼的數據,非常容易將其轉換回(解碼)原始文本。我們「只是改變了數據的表示」,即編碼。
在本質上,Base64編碼使用一組特定的、減少的字符來「編碼二進制數據」,以防止數據損壞。
Base64字母表
由于只有64個字符可用于編碼,我們可以僅使用6位來表示它們,因為2^6 = 64。每個Base64數字表示6位數據。一個字節中有8位,而 8 和 6 的「最小公倍數」是 24。因此,「24 位,或 3 個字節,可以用四個 6 位的 Base64 數字表示」。
4. Base64 使用案例
我們可能在HTML文檔中使用了<img src="789.jpeg">標簽來包含圖像。其實,我們可以直接將「圖像數據」嵌入到 HTML 中,而不必使用外鏈!數據URL可以做到這一點,它們使用Base64編碼的文本來內聯嵌入文件。
<img src="data:image/gif;base64,xxxxbase64encodedtextxxxx" />
data:[<mime type
>][;charset=<charset>][;base64],<encoded data></encoded></charset
></mime>
另一個常見的用例是當我們需要在網絡上傳輸或存儲一些二進制數據,而網絡只能處理文本或ASCII數據時。這確保了數據在傳輸過程中保持不變。還有就是在 URL 中傳遞數據時,當數據包含不適合 URL 的字符時,此時Base64就有了用武之地。
Base編碼還在許多應用程序中使用,因為它使得可以使用文本編輯器來操作對象。
我們還可以使用 Base64 編碼「將文件作為文本傳輸」。
- 首先,獲取文件的字節并將它們「編碼為 Base64」。
- 然后傳輸 Base64 編碼的字符串,然后在接收端「解碼為原始文件內容」。
5. Base64 編碼算法
以下是將一些文本轉換為 Base64 的簡單算法。
- 將文本轉換為其二進制表示。
- 將比特位分組為每組6位。
- 將每個組轉換為0到63的十進制數。它不能大于 64,因為每組只有 6 位。
- 如果轉換為十進制數的數字大于 64,我們可以將其取模64 例如:151 % 64 = 23
- 使用Base64字母表將此十進制數轉換為等效的Base64字符。
通過上述操作我們會得到一個Base64編碼的字符串。如果最后一組中的比特位不足,可以使用=或==作為填充。
讓我們以front7作為范例,來模擬上述操作。
- 通過首先將每個字符轉換為其對應的ASCII數字,然后將該十進制數轉換為二進制,(使用ASCII 轉二進制工具[4])將文本front7轉換為二進制:
01100110 01110010 01101111 01101110 01110100 00110111
f r o n t 7
- 將比特位分組為每組6位:
011001 100111 001001 101111 011011 100111 010000 110111
- 將每個組轉換為 0 到 63 之間的十進制數:
011001 100111 001001 101111 011011 100111 010000 110111
25 23 9 47 27 23 16 27
- 這步中如果數據超過 64,需要對其 64 取模
- 現在使用Base64字母表將每個十進制數轉換為其Base64表示:
25 23 9 47 27 23 16 27
Z n J v b n Q 3
然后我們完成了。名字front7在 Base64 中表示為ZnJvbnQ3。
乍一看,Base64 編碼的好處并不是很明顯。
想象一下,如果我們有一張圖片或一個「敏感文件」(PDF、文本、視頻等),而不是簡單的字符串,我們想將它存儲為文本。我們可以首先將其轉換為二進制,然后進行 Base64 編碼,以獲得相應的 ASCII 文本。
現在我們可以將該文本發送或存儲在任何地方,以任何我們喜歡的方式,而不必擔心一些舊設備、協議或軟件會錯誤解釋原始二進制數據以損壞我們的文件。
6. 如何進行 Base64 編碼和解碼
所有編程語言都支持將數據編碼為 Base64 格式以及從 Base64 格式解碼數據。
JS 中處理
// 簡單字符串
const text1 = "front789";
bota(text1); // ZnJvbnQ3ODk=
// 超出`Latin-1`字符的字符串
const text2 = "前端柒八九";
const encoder = new TextEncoder();
const data = encoder.encode(text);
const base64 = btoa(String.fromCharCode(...data));
console.log(base64); //5YmN56uv5p+S5YWr5Lmd
Rust 中處理
用Rust的話,我們可以直接用 base64 crate。
在 Cargo.toml 文件中添加以下內容:
[dependencies]
base64 = "0.21.5"
use base64::{Engine as _, engine::general_purpose};
let orig = b"data";
let encoded: String = general_purpose::STANDARD_NO_PAD.encode(orig);
assert_eq!("ZGF0YQ", encoded);
assert_eq!(orig.as_slice(), &general_purpose::STANDARD_NO_PAD.decode(encoded).unwrap());
// or, URL-safe
let encoded_url = general_purpose::URL_SAFE_NO_PAD.encode(orig);
想了解更多關于Rust如何處理Base64,可以查看Rust base64[5]
此外,終端也內置支持 Base64 編碼。在終端中嘗試以下命令:
echo "前端柒八九" | base64
5YmN56uv5p+S5YWr5LmdCg==
$ echo "5YmN56uv5p+S5YWr5LmdCg==" | base64 -d
前端柒八九