一份簡明的 Base64 原理解析

書接上回,在 記一個 Base64 有關的 Bug 一文裡,我們說到了 Base64 的編解碼器有不同實現,交叉使用它們可能引發的問題等等。

這一回,我們來對 Base64 這一常用編解碼技術的原理一探究竟。

1. Base64 是什麼

Base64 是一種基於 64 個可打印字符來表示二進制數據的表示方法。由於 2^6=64,所以每 6 個比特為一個單元,對應某個可打印字符。3 個字節有 24 個比特,對應於 4 個 Base64 單元,即 3 個字節可由 4 個可打印字符來表示。

——維基百科

它不是一種加解密技術,是一種簡單的編解碼技術。

Base64 常用於表示、傳輸、存儲二進制數據,也可以用於將一些含有特殊字符的文本內容編碼,以便傳輸。

比如:

  1. 在電子郵件的傳輸中,Base64 可以用來將 binary 的字節序列,比如附件,編碼成 ASCII 字節序列;
  2. 將一些體積不大的圖片 Base64 編碼後,直接內嵌到網頁源碼裡;
  3. 將要傳遞給 HTTP 請求的參數做簡單的轉換,降低肉眼可讀性;注:用於 URL 的 Base64 非標準 Base64,是一種變種。
  4. 網友們在論壇等公開場合習慣將郵箱地址 Base64 後再發出來,防止被爬蟲抓取後發送垃圾郵件。

2. Base64 編碼原理

標準 Base64 裡的 64 個可打印字符是 A-Za-z0-9+/,分別依次對應索引值 0-63。索引表如下:

一份簡明的 Base64 原理解析

編碼時,每 3 個字節一組,共 8bit*3=24bit,劃分成 4 組,即每 6bit 代表一個編碼後的索引值,劃分如下圖所示:

一份簡明的 Base64 原理解析

這樣可能不太直觀,舉個例子就容易理解了。比如我們對 cat 進行編碼:

一份簡明的 Base64 原理解析

可以看到 cat 編碼後變成了 Y2F0。

如果待編碼內容的字節數不是 3 的整數倍,那需要進行一些額外的處理。

如果最後剩下 1 個字節,那麼將補 4 個 0 位,編碼成 2 個 Base64 字符,然後補兩個 =:

一份簡明的 Base64 原理解析

如果最後剩下 2 個字節,那麼將補 2 個 0 位,編碼成 3 個 Base64 字符,然後補一個 =:

一份簡明的 Base64 原理解析

3. 實現一個簡易的 Base64 編碼器

講完原理,我們就可以動手實現一個簡易的標準 Base64 編碼器了,以下是我參考 Java 8 的 java.util.Base64 亂寫的一個 Java 版本,僅供參考,主要功能代碼如下:

<code>public class CustomBase64Encoder {

/**
* 索引表
*/
private static final char[] sBase64 = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'
};

/**
* 將 byte[] 進行 Base64 編碼並返回字符串
* @param src 原文
* @return 編碼後的字符串
*/
public static String encode(byte[] src) {
if (src == null) {
return null;
}

byte[] dst = new byte[(src.length + 2) / 3 * 4];

int index = 0;

// 每次將 3 個字節編碼為 4 個字節
for (int i = 0; i < (src.length / 3 * 3); i += 3) {
int bits = (src[i] & 0xff) << 16 | (src[i + 1] & 0xff) << 8 | (src[i + 2] & 0xff);
dst[index++] = (byte) sBase64[(bits >>> 18) & 0x3f];

dst[index++] = (byte) sBase64[(bits >>> 12) & 0x3f];
dst[index++] = (byte) sBase64[(bits >>> 6) & 0x3f];
dst[index++] = (byte) sBase64[bits & 0x3f];
}

// 處理剩下的 1 個或 2 個字節
if (src.length % 3 == 1) {
int bits = (src[src.length - 1] & 0xff) << 4;
dst[index++] = (byte) sBase64[(bits >>> 6) & 0x3f];
dst[index++] = (byte) sBase64[bits & 0x3f];
dst[index++] = '=';
dst[index] = '=';
} else if (src.length % 3 == 2) {
int bits = (src[src.length - 2] & 0xff) << 10 | (src[src.length - 1] & 0xff) << 2;
dst[index++] = (byte) sBase64[(bits >>> 12) & 0x3f];
dst[index++] = (byte) sBase64[(bits >>> 6) & 0x3f];
dst[index++] = (byte) sBase64[bits & 0x3f];
dst[index] = '=';
}

return new String(dst);
}
}
/<code>

這部分源碼我也上傳到 GitHub 倉庫 https://github.com/mzlogin/spring-practices 的 base64test 工程裡了。

4. 其它知識點

4.1 為什麼有的編碼結果帶回車

在電子郵件中,根據 RFC 822 規定,每 76 個字符需要加上一個回車換行,所以有些編碼器實現,比如 sun.misc.BASE64Encoder.encode,是帶回車的,還有 java.util.Base64.Encoder.RFC2045,是帶回車換行的,每行 76 個字符。

4.2 Base64 的變種

除了標準 Base64 之外,還有一些其它的 Base64 變種。

比如在 URL 的應用場景中,因為標準 Base64 索引表中的 / 和 + 會被 URLEncoder 轉義成 %XX 形式,但 % 是 SQL 中的通配符,直接用於數據庫操作會有問題。此時可以採用 URL Safe 的編碼器,索引表中的 /+ 被換成 -_,比如 java.util.Base64.Encoder.RFC4648_URLSAFE 就是這樣的實現。

5. 參考鏈接

  • https://zh.wikipedia.org/zh-hans/Base64
  • https://www.liaoxuefeng.com/wiki/897692888725344/949441536192576


分享到:


相關文章: