【字元編碼系列】Base64編碼原理以及實現

weixin_34127717發表於2017-12-18

寫在前面的話

本文屬於 字元編碼系列文章之一,更多請前往 字元編碼系列

大綱

  • 簡介
  • 編碼原理
  • 轉碼對照表
  • 示例步驟分析
  • 原始碼實現

簡介

Base64是一種編碼方式,通常用於將二進位制資料編碼為可列印字元組成的資料格式。

為什麼要有Base64編碼

在很久以前,傳送郵件時只支援ASCII字元的傳送,如果有非ASCII碼字元,則傳送不了,於是需要在不改變傳統協議的情況下,做一種擴充套件方案來支援這類字元的傳送。Base64編碼應運而生。

Base64的常見誤區

很多開發者喜歡直接用Base64進行加密解密工作,實際上這個是完全無意義的,因為Base64這種編碼規則是公開的,基本只要有程式能力都能解開,所以請勿用作加密用途。

Base64編碼的主要的作用不在於安全性,而在於讓內容能在網路間無錯的傳輸。(常用語編碼特殊字元,編碼小型二進位制檔案等)

編碼原理

  • 將資料按照 3個8位位元組一組的形式進行處理,每3個8位位元組在編碼之後被轉換為4個6位位元組

    • 3*8=24變為4*6=24
    • 在編碼後的6位的前面補兩個0,形成8位一個位元組的形式
    • 這樣,編碼後3個8位位元組則自動轉化成4個6位位元組了
    • 原因是2的6次方為64,所以每6個位為一個單元,可以轉換為對應64個字元中的某一個
  • 當資料的長度無法滿足3的倍數的情況下,最後的資料需要進行填充操作

    • 當原資料不是3的整數倍時,會自動補0。也就是說,如果原資料剩餘1個位元組,那麼,另外兩個都是補的0,如果剩餘2個位元組,另外一個位元組補得0。
    • 然後編碼時,對於後面自動補0的字元,會用=作為填充字元(這裡=不是第65個字元,僅僅做填充作用)
    • 之所以要用=號進行填充是為了解碼時方便還原(因為=號只需要還原為0即可)

解碼

  • 解碼是編碼的逆過程
  • 其中的=號還原為0即可

轉碼對照表

每6個單元高位補2個零形成的位元組位於0~63之間,通過在轉碼錶中查詢對應的可列印字元。“=”用於填充。如下所示為轉碼錶。

示例步驟分析

以”Word”字串的編碼和解碼為例。

編碼

├  原始字元 | W | o | r | d(由於不是3的倍數,所以要補0) |
├─────────────────────────────────|
├ ASCII碼 |  87 | 111 | 114 | 100 |
├─────────────────────────────────|
├ 8bit位元組 |  01010111 | 01101111 | 01110010 | 01100100 | 00000000 | 00000000 |
├─────────────────────────────────|
├ 6bit位元組 |  010101 | 110110 | 111101 | 110010 | 011001 | 000000 | 000000 | 000000 |
├─────────────────────────────────|
├ B64十進位制 |  21 | 54 | 61 | 50 | 25 | 0(注意,這裡有兩位是d裡面的,所以是正常的0) | 異常(需要補上=號) | 異常 |
├─────────────────────────────────|
├ 對應編碼 |  V | 2 | 9 | y | Z | A | = | = |
└───────────────────────────────────────────

所以’Word’的編碼結果是V29yZA==

解碼

├ 原始編碼 |  V | 2 | 9 | y | Z | A | = | = |
├─────────────────────────────────|
├ B64十進位制 |  21 | 54 | 61 | 50 | 25 | 0 | 異常 | 異常 |
├─────────────────────────────────|
├ 6bit位元組 |  010101 | 110110 | 111101 | 110010 | 011001 | 000000 | 000000 | 000000 |
├─────────────────────────────────|
├ 8bit位元組 |  01010111 | 01101111 | 01110010 | 01100100 | 00000000 | 00000000 |
├─────────────────────────────────|
├ ASCII碼 |  87 | 111 | 114 | 100 |異常 |異常 |
├─────────────────────────────────|
├  對應字元 | W | o | r | d | 無 | 無 |
└─────────────────────────────────────

由此可見,解碼過程就是編碼過程的逆過程。

原始碼實現

需要注意的是,實際編碼時需要注意程式內部的編碼,例如Javascript內建的編碼現在可以看成是UTF-16,所以如果編碼成GBK或UTF-8時需要經過一定的轉換

/**
 * @description 建立一個base64物件
 */
(function(base64) {
    /**
     * Base64編碼要求把3個8位位元組(3*8=24)轉化為4個6位的位元組(4*6=24),
     * 之後在6位的前面補兩個0,形成8位一個位元組的形式。
     * 由於2的6次方為64, 所以每6個位為一個單元, 對應某個可列印字元。
     * 當原資料不是3的整數倍時, 如果最後剩下兩個輸入資料,
     * 在編碼結果後加1個“=;如果最後剩下一個輸入資料,編碼結果後加2個“=;
     * 如果沒有剩下任何資料,就什麼都不要加,這樣才可以保證資料還原的正確性。
     */
    /**
     * base64轉碼錶,最後一個=號是專門用來補齊的
     */
    var keyTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
    /**
     * @description 將一個目標字串編碼為base64字串
     * @param {String} str 傳入的目標字串
     * 可以是任何編碼型別,傳入什麼型別就輸出成了什麼樣的編碼
     * 由於js內建是utf16編碼,而伺服器端一般不使用這種,
     * 所以傳入的編碼一般是採取utf8或gbk的編碼
     * @return {String} 編碼後的base64字串
     */
    function encodeBase64(str) {
        if (!str) {
            return '';
        }
        // 遍歷索引
        var i = 0;
        var len = str.length;
        var res = [];
        var c1, c2, c3 = '';
        // 用來存對應的位置
        var enc1, enc2, enc3, enc4 = '';
        while (i < len) {
            c1 = str.charCodeAt(i++) & 0xFF;
            c2 = str.charCodeAt(i++);
            c3 = str.charCodeAt(i++);
            enc1 = c1 >> 2;
            enc2 = ((c1 & 0x3) << 4) | ((c2 >> 4) & 0x0F);
            enc3 = ((c2 & 0x0F) << 2) | ((c3 & 0xC0) >> 6);
            enc4 = c3 & 0x3F;
            // 專門用來補齊=號的
            if (isNaN(c2)) {
                enc3 = enc4 = 0x40;
            } else if (isNaN(c3)) {
                enc4 = 0x40;
            }
            res.push(keyTable.charAt(enc1));
            res.push(keyTable.charAt(enc2));
            res.push(keyTable.charAt(enc3));
            res.push(keyTable.charAt(enc4));
            c1 = c2 = c3 = "";
            enc1 = enc2 = enc3 = enc4 = "";
        }
        return res.join('');
    };
    /**
     * @description 解碼base64字串,還原為編碼前的結果
     * @param {String} str 傳入的目標字串
     * 可以是任何編碼型別,傳入什麼型別就輸出成了什麼樣的編碼
     * 由於js內建是utf16編碼,而伺服器端一般不使用這種,
     * 所以傳入的編碼一般是採取utf8或gbk的編碼
     * @return {String} 編碼後的base64字串
     */
    function decodeBase64(str) {
        if (!str) {
            return '';
        }
        // 這裡要判斷目標字串是不是base64型,如果不是,直接就不解碼了
        // 兩層判斷
        if (str.length % 4 != 0) {
            return "";
        }
        var base64test = /[^A-Za-z0-9\+\/\=]/g;
        if (base64test.exec(str)) {
            return "";
        }
        var len = str.length;
        var i = 0;
        var res = [];
        var code1, code2, code3, code4;
        var c1, c2, c3 = '';
        while (i < len) {
            code1 = keyTable.indexOf(str.charAt(i++));
            code2 = keyTable.indexOf(str.charAt(i++));
            code3 = keyTable.indexOf(str.charAt(i++));
            code4 = keyTable.indexOf(str.charAt(i++));

            c1 = (code1 << 2) | (code2 >> 4);
            c2 = ((code2 & 0xF) << 4) | (code3 >> 2);
            c3 = ((code3 & 0x3) << 6) | code4;

            res.push(String.fromCharCode(c1));

            if (code3 != 64) {
                res.push(String.fromCharCode(c2));
            }
            if (code4 != 64) {
                res.push(String.fromCharCode(c3));
            }
        }
        return res.join('');
    };

    /**
     * @description 將字串進行base64編碼,之後再進行uri編碼
     * @param {String} str 傳入的utf16編碼
     * @param {String} type 類別,是utf8,gbk還是utf16,預設是utf16
     * @param {Boolean} isUri 是否uri編碼
     * @return {String} 編碼後並uri編碼的base64字串
     */
    base64.encode = function(str, type, isUri) {
        type = type || 'utf16';
        if (type == 'gbk') {
            // 轉成 gbk
            str = exports.utf16StrToGbkStr(str);
        } else if (type == 'utf8') {
            // 轉成 utf8
            str = exports.utf16StrToUtf8Str(str);
        }
        // 否則就是預設的utf16不要變
        // 先b64編碼,再uri編碼(防止網路傳輸出錯)
        var b64Str = encodeBase64(str);
        if (isUri) {
            b64Str = encodeURIComponent(b64Str);
            console.log(b64Str);
        }
        return b64Str;
    };

    /**
     * @description 將字串先進行uri解碼,再進行base64解碼
     * @param {String} str 傳入的編碼後的base64字串
     * @param {String} type 類別,是utf8,gbk還是utf16,預設是utf16
     * @param {Boolean} isUri 是否uri解碼
     * @return {String} 編碼後並uri編碼的base64字串
     */
    base64.decode = function(str, type, isUri) {
        type = type || 'utf16';
        if (isUri) {
            str = decodeURIComponent(str);
        }
        var decodeStr = decodeBase64(str);
        if (type == 'gbk') {
            return exports.gbkStrToUtf16Str(decodeStr);
        } else if (type == 'utf8') {
            return exports.utf8StrToUtf16Str(decodeStr);
        }
        // 否則就是預設的utf16不要變
        return decodeStr;
    };
})(exports.Base64 = {});

原始碼

詳細可以參考原始碼: https://github.com/dailc/charset-encoding-series

附錄

部落格

初次釋出2017.06.10於個人部落格

http://www.dailichun.com/2017/06/10/base64encoding.html

參考資料

相關文章