寫在前面的話
本文屬於 字元編碼系列文章之一,更多請前往 字元編碼系列。
大綱
- 簡介
- 編碼原理
- 轉碼對照表
- 示例步驟分析
- 原始碼實現
簡介
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