Base 64 编码

Base64 是一种用 64 个可打印字符来表示任意二进制数据的二进制编码方法。可以用来处理和传输任意二进制数据。

Base64 最初是用在电子邮件中,为了满足电子邮件中不能直接使用非 ASCII 码字符的规定,用来传输二进制文件的。除此之外,它还可将任意不可打印的二进制数据,转化为可打印的文本编码,使用文本软件进行编辑(二进制流中有很多都是无法显示和打印的,比如 ASCII 中的控制字符,二进制文件 jpg、pad、exe 等,如果用记事本打开这些文件,会看到一堆乱码)。

原理

Base64 编码将一个 8 位字节序列拆散为 6 位的片段,并为每个 6 位的片段分配一个字符,这个字符是 Base64 字母表中的 64 个字符之一。这 64 个输出字符都是很常见的 ASCII 字符,包括大小写字母、数字、+ 和 /,还使用了特殊字符 =。

Base64 编码表 (The Base64 Alphabet):

索引 对应字符 索引 对应字符 索引 对应字符 索引 对应字符
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y

还可以自己定义 64 个字符的排列顺序,这样就可以自定义 Base64 编码,不过,通常情况下没有必要。

注意:由于 Base64 编码用了 8 位字符来表示信息中的 6 个位,所以 Base64 编码字符串大约比原始值增加了 33%,(8 - 6) / 6 = 1/3,选择 Base64 编码时要考虑一下文件的 size。另外,Base64 是一种通过查表的数据编码方法,目的是让数据符合传输协议的要求,不能将其误用于数据加密或数据校验,即使使用自定义的编码表也不行。

编码实现

下面是一个简单的 Base64 编码实例。在这里,三个字符组成的输入值 “Ow!” 是 Base64 编码的,得到的是 4 个字符的 Base64 编码值 “T3ch”。它是按以下方式工作的。

8 位字符 O w !
8 位值(十六进制) $4F $77 $21
8 位值(二进制) 01001111 01110111 00100001
对上面 8 位值(二进制)进行拆分 - - - -
6 位值(二进制) 010011 110111 011100 100001
6 位值(十进制) 19 55 28 33
Base64 字符 T 3 c h
1
2
3
4
* 字符串 "Ow!" 被拆分成 3 个 8 位的字节 (0x4F、0x77、0x21);
* 这 3 个字节构成了一个 24 位的二进制 01001111 01110111 00100001;
* 这些位被划分为一些 6 位的序列 010011、110111、011100、100001;
* 每个 6 位值都表示了从 0~63 之间的一个数字,对应 Base64 字母表中的 64 个字符之一。得到的 Base64 编码字符串是 4 个字符的字符串 "T3ch"。然后就可以通过线路将这个字符串作为“安全的” 8 位字符传送出去,因为只用了一些移植性最好的字符(字母、数字等);
  • 用 JavaScript 描述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var str = 'Ow!';

var binary = [];
for (var i = 0; i < str.length; i++) {
var binStr = str.charCodeAt(i).toString(2); // charCodeAt 返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数
binary.push(binStr); // ['1001111', '1110111', '100001']
}

// 一个正常的字节都是由 8bit 组成的,不够 8bit 需要在高位补 0,于是得到 ['01001111', '01110111', '00100001']
// 1 把字符串按照 6 位分开,进行分割,得到 ['010011', '110111', '011100', '100001']
// 2 将每一个转换为十进制分别对于 [19, 55, 28, 33];
// 3 将每一位数字分别对于上面提供的 Base64 对应表,得到对应的编码,分别对于 T3ch
// 4 最后就会得到 Base64 编码 T3ch

console.log('字符 "Ow!" 最后得到的 Base64 编码为" T3ch"');
  • 前提 – 确定具体的编码方案

需要注意的是,ASCII 码 128 位后的字符,各个编码方案不兼容,比如汉字有 gb2312、utf-8、gbk 等,每一种编码的 Base64 对应值都不一样,是用 Base64 进行编码前,需要确定编码方案。下面以’严’的 UTF-8 为例:

“严”的 UTF-8 编码为 E4B8A5,写成二进制就是三字节的 “11100100 10111000 10100101”。将这个 24 位的二进制字符串,转换成四组一共 32 位的二进制值”111001 001011 100010 100101”,相应的十进制数为 57、11、34、37,它们对应的 Base64 值就为5、L、i、l。

1
2
3
// NodeJS(btoa 不支持中文)
var b = new Buffer('严');
b.toString('base64'); // 5Lil

填充

Base64 编码收到一个 8 位字节序列,将这个二进制序列流划分成 6 位的块。二进制序列有时不能正好平均地分为 6 位的块,在这种情况下,就在序列末尾填充零位,使二进制序列的长度成为 24 的倍数(6 和 8 的最小公倍数)。

对已填充的二进制进行编码时,任何完全填充(不包括原始数据中的位)的 6 位组都有特殊的第 65 个符号 “=” 表示。如果 6 位组是部分填充的,就将填充位设置为 0。

下面是一个填充实例:

输入数据 二进制序列(填充位以 x 表示) 已编码数据
a:a 011000 010011 101001 100001 YTph
a:aa 011000 010011 101001 100001 011000 01xxxx xxxxxx xxxxxx YTphYQ==
a:aaa 011000 010011 101001 100001 011000 010110 0001xx xxxxxx YTphYWE=
a:aaaa 011000 010011 101001 100001 011000 010110 000101 1000001 YTphYWFh

初始输入字符串为 “a:a” 为 3 个字节(24 位)。24 是 6 和 8 的倍数,因此按照上面给出的例子计算。无需填充就会得到 Base64 编码为 “YTph”。

然而,再增加一个字符,输入字符串变为 “a:aa”,转换为二进制就会有 32 位长。而 6 和 8 的下一个公倍数为 48,因此要添加 16 位的填充码。填充的前 4 位是与数据位混合在一起的。得到的 6 位组 01xxxx,会被当作 010000、十进制中的 16,或者 Base64 编码的 Q 来处理。剩下的两个 6 位组都是填充码,用 = 来表示。

‘O’ 的填充实例:

1
2
3
4
5
6
7
8
9
10
11
12
'O'.charCodeAt().toString(2);
得到二进制(位数不够 8 位时,需要在高位补码)
1001111 --> 01001111
-----------------------------------------
按 6 位拆分二进制(位数不是 24 的倍数时,需要低位补码)
010011, 11xxxx, xxxxxx, xxxxxx
-----------------------------------------
转为十进制
19, 48
-----------------------------------------
Base64 查表结果
Tw==

浏览器中 Base64 编码生成

字符的编码

Web API 中 atobbtoa 用来对字符进行 Base64 编码和解码。btoa 将二进制的字节数据(binary bytes)编码成 ASCII 字符串,atob 解码通过 Base64 编码的字符串数据。

1
2
btoa('a:a'); // YTph
atob('YTph'); // a:a

注:btoaatob 分别是 Binary to ASCII 和 ASCII to binary 的缩写。b 不是 Base64 的缩写,计算机中的所有数据的本质都是二进制,我们所看到的文字、符号、图片、语音视频…,都是二进制数据在具体编码(字符编码、图像编码…)下的表现。

IE9 不支持 atobbtoa,可手动实现 Base64 编解码函数:js-base64base64

  • 编码 Unicode 字符

btoa 仅仅支持 Latin1 范围的字符(其编码范围是 0x00-0xFF0x00-0x7F 之间完全和 ASCII 一致,0x80-0x9F 之间是控制字符,0xA0-0xFF 之间是文字符号),不能编码 Unicode 字符,对 Unicode 字符串进行编码都会触发字符越界的异常。

1
2
var str = 'Hello, 中国!';
btoa(str); // Uncaught DOMException: ...The string to be encoded contains characters outside of the Latin1 range.

原因是这个函数将每个字符视为二进制数据的字节,而不管实际组成字符的字节数是多少,所以如果任何字符的码位超出 0x00 ~ 0xFF 范围,则会引发 InvalidCharacterError 异常。这只是 btoa 这个方法的局限,并不是 Base64 的局限,Base64 可以编码所有的二进制数据。

解决办法是先把 Unicode 字符串转换为 UTF-8 编码。

1
2
btoa(encodeURIComponent('Hello, 中国!')); // SGVsbG8lMkMlMjAlRTQlQjglQUQlRTUlOUIlQkQlRUYlQkMlODE=
decodeURIComponent(atob('SGVsbG8lMkMlMjAlRTQlQjglQUQlRTUlOUIlQkQlRUYlQkMlODE=')); // Hello, 中国!

文件的编码

dataURL 是 data 类型的 URL,指含有 Base64 数据的 URL,用于在 URL 中使用二进制数据,在 RFC2397 中提出,完整的格式为:

1
2
data:文件类型;编码方式,编码后的文件内容
data: [ mediatype ] [ ";base64" ] "," data

比如,文本和图片的 dataURL 表示:

1
2
data:text/plain;charset=UTF-8;base64,5L2g5aW977yM5Lit5paH77yB
data:image/gif;base64,xxxxxxxxxx

dataURL 在 img srcbackground urllink hrefscript src… 中使用。

  • dataURL 生成

Web API 中的 FileReader.readAsDataURL 可以将一个文件转为 dataURL,图片还可以使用 canvas 的 canvas.toDataURL 生成。将一个 dataURL 转为 Blob 或者 FiledataURLtoBlobdataURLtoFile

URL Safe

由于标准的 Base64 编码可能会出现 +/= 字符,这在 URL、Cookie 中不能直接作为参数,会造成歧义,所以又有一种 “URL Safe” 的 Base64 编码,其实就是把字符 +/ 分别变成 -_,把 = 去掉。

1
2
3
4
5
6
7
8
base64.b64encode('i\xb7\x1d\xfb\xef\xff') # abcd++//
base64.urlsafe_b64encode('i\xb7\x1d\xfb\xef\xff') # abcd--__
base64.urlsafe_b64decode('abcd--__') # i\xb7\x1d\xfb\xef\xff

base64.b64decode('YWJjZA==') # abcd
base64.b64decode('YWJjZA') # 出错

safe_b64decode('YWJjZA') # abcd

Base64 的应用

网络传输协议中

Base64 的用途之一就是便于用文本或二进制文件数据的传送。 在网络传输中,Base64 一般适合以下场合的使用:

1
2
* 传输信道只支持 ASCII 字符,不方便传输二进制流的场合;
* 含有非 ASCII 字符(127 位以后字符),容易出现编码问题的场合;
  • SMTP

Base64 最早就是用来邮件传输协议(SMTP)中的,是作为 MIME 多媒体电子邮件标准的一部分开发的,原因是邮件传输协议是一个文本协议,只支持 ASCII 字符(纯文本,可打印)传输,如果要传输二进制文件(比如邮件附件中的图像、声音等)和非 ASCII 字符,就需要用 Base64 将二进制文件内容和非 ASCII 字符编码为只包含 ASCII 字符的内容,并指定 MIME(多用途互联网邮件扩展类型)(详情参考 HTTP 里面相关说明)。

例如,正文为空,带一个名为 hello.txt 的附件,内容为您好!世界!。导出邮件源码,其关键部分如下所示:

1
2
3
4
5
MIME-Version: 1.0                           # 表示当前使用MIME标准1.0版本
Content-Type: text/plain; name="hello.txt" # 表示附件文件名为 hello.txt ,格式为纯文本

Content-Transfer-Encoding: base64 # 表示附件文件内容使用 base64 编码后传输。可选值 7bit、8bit、binary、quoted-printable、base64、custom
5oKo5aW977yM5LiW55WM77yB # 文件内容 您好,世界! Base64编码后的结果

不过,MIME 使用的不是标准 Base64 编码。

  • HTTP

HTTP 是超文本传输协议,可以传输纯文本(底层也是二进制流),也可以直接传输二进制数据流。在传输纯文本的时候,由于有些数据格式不符合协议本身的规范,这时候就需要对这些数据进行编码处理成安全格式,从而可以合法地作为首部字段和正文的值。

1
2
特殊字符:=、空格、:、+、/、换行符...;
非 ASCII 码:比如中文;

当然,在 HTTP 中,URI 编码一般都会用配套的 URIencode,而不使用 Base64,除非需要编码二进制文件为 DataURL。对于 URL 的编码,请看相关章节。

注:HTTP 也支持 MIME,请查看相关章节。

dataURL

  • img src、background url、link href、script src、图片预览和上传

注:图片预览和上传这个需求用 Blob 更合理,Blob 通过 URL.createObjectURL 也能预览。

X.509 公钥证书、HTTP 基本认证和摘要认证

基本认证摘要认证