Skip to content
0

Base64

Base64 是一种将二进制数据转换为文本的优雅方法,主要用于对 8 bit(一个字节)为单位的数据进行编码

编码

通过 Base64 编码的出来的文本只包含 a-zA-Z0-9+/,共 26 + 26 + 10 + 2 等于 64 个字符,这就是为什么它被称为 Base64

在 JS 中,浏览器和 NodeJs 都直接提供了编码的 API:

btoa("peterroe") //=> cGV0ZXJyb2U=

atob("cGV0ZXJyb2U=") //=> peterroe

即然 Base64 的文本字符至多只有 64 种,所以我们给每一个字符重新编码:

由于只有 64 个字符可以编码,而且 2^6 = 64,所以每个字符可以用 6 个 bit 表示

编码算法

"peterroe" 这个字符串举例子,在 ASCII 中的编码是:

01110000 01100101 01110100 01100101 01110010 01110010 01101111 01100101
    p        e        t        e        r        r        o        e

我们将上面的 8 bit 一组在拆分为 6 bit 一组:

011100 000110 010101 110100 011001 010111 001001 110010 011011 110110 0101

在代入我们上面的字母表,就得到对应的 Base64 编码:

011100 000110 010101 110100 011001 010111 001001 110010 011011 110110 0101(00) 不足6位补0
   c      G      V      0      Z      X      J      y      b      2     U

但是 Base64 还有一个条件,得到的结果长度需要满足是 4 的倍数, 而"cGV0ZXJyb2U".length % 4 = 3,所以需要在后面补 1 个 = ,得到最终结果 cGV0ZXJyb2U=

根据上面的编码规则计算,可以算出编码后的字符大约比编码前大了 33%,但是这个代价换来的是安全

安全性

为什么被选择加入字母表的 64 个字符是这 26 个?因为它们被认为是安全的

ASCII 码的 128 ~ 255 之间的值是不可见字符,在网络传输过程中,这些字符很有可能因为设备对字符的处理方式不同,导致传输的数据出错

URL安全的 base64

有上面 Base64 的字符集可以看到,编码的结果可能包含 +/=,而这些字符是 URL 的保留字符或不安全字符,所以直接将 Base64 的结果放到 URL 中是不安全

根据 RFC4648 描述,在 URL 安全的 Base64 编码中,将 + 替换为中划线 -,将 / 替换为下划线 _,所以我们有第二张编码表:

所以通过如下方法,可以得到 URL 安全的 Base64 编码

btoa("data").replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

但是 btoa 不是完美的

btoa 的限制

在 JS 中要使用 btoa 要小心谨慎,根据 MDN-btoa 中的介绍

btoa() 函数将一个 JavaScript 字符串作为其参数。而 JavaScript 字符串使用 UTF-16 字符编码表示:在这种编码中,字符串使用一串 16 比特(2 字节)的单元来表示。每一个 ASCII 字符会被填充到每个单元的第一个字节中,而很多其他的字符则不然

根据设计,Base64 仅将二进制数据作为其输入。而在 JavaScript 字符串中,这意味着每个字符只能使用一个字节表示。所以,如果你将一个字符串传递给 btoa(),而其中包含了需要使用超过一个字节才能表示的字符,你就会得到一个错误,因为这个字符串不能被看作是二进制数据

这意味着,比如字符串中包含像 这样的非 ASCII 范围的字符,则会抛出错误

btoa("") // throws exception

URL 的绝对安全

encodeURIComponent

为了追寻 URL 的绝对安全,在 JS 中,可以使用 encodeURIComponent,对任意包括中文等特殊字符进行编码

const b64 = btoa("peterroe")            //=> cGV0ZXJyb2U=
const safeB64 = encodeURIComponent(b64) //=> cGV0ZXJyb2U%3D

const encodedChars = encodeURIComponent("🌟8$K!ПрB#y2&🚀C9zち1qحبًp")
// => '%F0%9F%8C%9F8%24K!%D0%9F%D1%80B%23y2%26%F0%9F%9A%80C9z%E3%81%A11q%D8%AD%D8%A8%D9%8Bp'
decodeURIComponent(encodedChars)
// => '🌟8$K!ПрB#y2&🚀C9zち1qحبًp'

encodeURIComponent 对所有特殊字符进行编码,这很有用,但是某些情况下我们可能不需要将特殊字符全部编码,尤其是编码一个 URI:

encodeURIComponent("https://peterroe.icu? hello")
//=> 'https%3A%2F%2Fpeterroe.icu%3F%20hello'

上面使用 encodeURIComponent之后,:/ 等字符串也进行了编码,导致浏览器不能直接识别这个 URI,所以 JS 还提供了另一个方法: encodeURI

encodeURI

用于对整个 URI 进行编码,但会保留某些字符,如冒号 :、正斜杠 /、问号? 和井号 #,因为它们在 URI 中具有特殊含义

const encodedChars = encodeURI("https://peterroe.icu? hello")
// => 'https://peterroe.icu?%20hello'
decodeURI(encodedChars)
// => 'https://peterroe.icu? hello'

这样就可以在保证 URI 合法的情况下,编码特殊字符

TIP

encodeURI 适合对 URI 整体进行编码,而 encodeURIComponent 适合局部编码,这也符合它的名称(编码URI的组件)

二进制 和 Base64 转换

有的时候,需要从前端传入图片到后端,以及后端读取本地图片返回前端,这里是二者相互转换的方法

if (isBinaryObject(node.filename)) {
    const buffer = readFileSync(item.path, null);
    node.content = buffer.toString('base64');
} else {
    node.content = readFileSync(item.path, 'utf-8');
}

if(isBinaryObject(node.filename)) {
    // 需要去掉前缀
    const buffer = Buffer.from(node.content.replace(/^data:image\/\w+;base64,/, ''), 'base64');
    writeFileSync(node.path, buffer);
} else {
    writeFileSync(node.path, node.content, 'utf-8');
}

Released under the MIT License.