成為前端工程師的第二個月,我拿到了一個顯示 QRCode 的任務,在這個任務之前我從來沒有想過這些 QRCode 是怎麼來的,我擅自認為大概就是有一串網址,然後一個 QRCode 轉換的套件,給這個套件網址列,就可以拿到生成的 QRcode 這樣吧。

但是當我看到後端傳回來的東西的時候,我發現代誌不是戇人想的那麼簡單。 在 'qrcode' 的 key 底下,回傳了一串很像亂碼的很長的字串
大概是長這個樣子:

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

這個東西其實是 QRCode 圖片在 Base64 encode 之後產生的字串。透過這種方式顯示 QRcode,可以達成圖片優化的效果,這篇文章就想來看一下這個做法和背後牽涉到的原理。
這篇文章想來看看三個東西:Base64 是什麼、Data URIs 是什麼以及 HTML 應用。

Base64

Base64 是一種基於 64 個可列印字元來表示二進位資料的表示方法。這 64 個可列印字元包括 A-Z、a-z、數字 0-9,這樣共有 62 個字元,另外會再加上兩個可列印符號,這兩個符號在不同的系統會不同,不過最常見的是 /+
我們詳細看一下 Base64 的轉換過程

encode 方式

首先,我們想要使用的內容是 64 個可列印字元,這些字元在 ASCII 的轉換表上是長這樣的:

接著,基於 2 的 6 次方等於 64,我們可以以每 6 個 bit 為單位,創造一個限制在 64 內的小組,這些小組轉回來的樣子,就會是在我們想要的 64 個可列印字元之內。

維基百科提供了這樣的一個例子:
我們將「Man」進行 Base64 的編碼,首先,M A N 分別會對應到 ASCII 的 77、97 和 110。
將 77、97 和 110 分別用一個 byte ( 8 bit )表示,接著將這一共 24 個 bit 在進行 6 個一組,得到一組新的數字19、22、5 和 46,這一組數字就是 base64 的索引,用這些索引在 ASCII 中找到對應的文字 T、W、F、u 就是 Base64 編碼的結果。

另外,如果要編碼的 byte 數不能被 3 整除(結果無法為 4 個一組),那麼就會在最後補上一個或兩個 byte 的 0,並在生成的 Base64 字串最後補上一或兩個 = 表示。

為什麼需要使用 base64

雖然我沒有查到準確的原因,但 base64 很常被使用的一個場景,是在以 SMTP(Simple Mail Transfer Protocol) 為基礎的電子郵件通訊上。因為原本的電子郵件規範是基於文字創造的,所以這個規範在訂定時規定使用 7 個 bit,這使得它在處理二進制檔案傳輸時遇到一些困難。為了解決這個困難,當時會把附加檔案和圖片等等以 base64 轉換成可列印的字串,這樣就可以很容易的使用 SMTP 進行傳輸。
現在大多數 SMTP 伺服器已經都都援 8 位元 MIME 擴充,就沒有必要使用這個方式傳輸。另外,在 2018 年也有基於這種傳輸方式的漏洞被發現,這個漏洞讓攻擊者可以遠端執行程式,不過提供服務的 Exim 表示這個漏洞很難被利用,並且已經被修正。
有些像是垃圾郵件依然會使用 Base64 encode 信件內容,因為經過轉換的內容比較容易通過垃圾郵件的審查機制。

Data URIs

Data URIs 簡介

接著要介紹的是 Data URIs。
data URIs 是在 August 1998 新增的 RFC標準,rfc2397

A new URL scheme, "data", is defined. It allows inclusion of small
data items as "immediate" data, as if it had been included
externally.

在這個標準中,定義了一個新的 URL 格式。這個新的格式允許直接傳輸小型 data 的內容。

一般來說, URI(Uniform Resource Identifier) 通常是代表一個檔案位置的標示,我們需要再取得 URI 之後另外取得內容,然而,在這個新的 Scheme 出現之後,再取得 URI 的同時,我們就已經取得經過 base64 編碼的檔案內容了。

語法和範例

這個結構的語法長這樣
data:[<mediatype>][;base64],<data>

<mediatype> 中會填入媒體類型,接著是固定的 ;base64 表明他的 encode 方式,最後則是 encode 後的結果,這會是前面說倒的由可以印出來的 64 個字元組成的內容。

在標準中提供的範例長這樣:

data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw
   AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz
   ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp
   a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl
   ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis
   F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH
   hhx4dbgYKAAA7

<mediatype>image/gif,接著是編碼模式和結果。
直接把這串 URI 放到網址列,就可以看到這張圖片的內容。這時看 response 會發現並沒有拿到 response body,但圖片卻正確顯示,這是因為這張圖片已經被帶在 URI 中了。

和 HTML 結合

這種資料格式可以使用在 HTML 的 <img> 中。直接在 src 中放上內容,就可以正確顯示圖片。

用 Base64 顯示 QRCode 實例

知道了編碼方式和 DATA URI 格式,就可以理解這次使用 base64 顯示QRcode 的過程都發生了些什麼事。

首先,根據需要的網址轉換出 QRCode 之後,我們直接在後端對它進行 Base64 encode。
在前端發 request 時,直接回傳這串 encode 後的字串。
前端在拿到這一串字串之後,加上 data:<mediatype>;base64 的辨識符,放進 <img> 中,就可以顯示這張圖片。
這樣就完成整個過程了。

順帶一提,我一開始在最上面提供的超長字串,是我的部落格的 QRCode encode 後的字串,有興趣的朋友可以自己加上 DATA URI 的語法試試能不能正常顯示。
我使用的是
QR Code Generator
Base64 Image Encoder
這兩個簡單的轉換網站,有興趣的話也可以做做看自己的圖片來玩。

這篇文章就到這邊結束,我們下篇文章見拉!


參考資料:
簡單郵件傳輸協定 - 維基百科,自由的百科全書
Base64 - Wikipedia(英文)
Base64 - Wikipedia(日文)
Base64 - 維基百科,自由的百科全書
CVE-2018-6789