1. 介绍

Base64 是网络上最常见的用于传输 8Bit 字节码的编码方式之一,Base64 就是一种基于64个可打印字符来表示二进制数据的方法。可查看 RFC2045~RFC2049,上面有 MIME 的详细规范。

2. 编码原理

Base64 编码之所以称为 Base64,是因为其使用64个字符来对任意数据进行编码,同理有 Base32、Base16 编码。标准 Base64 编码所使用的的64个字符为:

file

这64个字符是各种字符编码(比如 ASCII 编码)所使用字符的子集,基本,并且可打印。唯一有点特殊的是最后两个字符,因对最后两个字符的选择不同,Base64 编码又有很多变种,比如 Base64 URL 编码。

Base64 编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续6比特(2的6次方=64)计算其十进制值,根据该值在上面的索引表中找到对应的字符,最终得到一个文本字符串。

假设我们要对 Hello! 进行 Base64 编码,按照 ASCII 表,其转换过程如下图所示:

file

可知 Hello! 的 Base64 编码结果为 SGVsbG8h ,原始字符串长度为6个字符,编码后长度为8个字符,每3个原始字符经 Base64 编码成4个字符,编码前后长度比 3/4,这个长度比很重要 —— 比原始字符串长度短,则需要使用更大的编码字符集,这并不是我们想要的;长度比越小,则需要传输越多的字符,传输时间越长。Base64 应用广泛的原因在于字符集大小与长度比之间取得一个较好的平衡,适用于各种场景。

但这里需要注意一个点:Base64 编码是每3个原始字符编码成4个字符,如果原始字符串长度不能被3整除,那怎么办?使用0值来补充原始字符串。

Hello!! 为例,其转换过程为:

file

Hello!! Base64 编码的结果为 SGVsbG8hIQAA 。最后 2 个零值只是为了 Base64 编码而补充的,在原始字符中并没有对应的字符,那么 Base64 编码结果中的最后两个字符 AA 实际不带有效信息,所以需要特殊处理,以免解码错误。

标准 Base64 编码通常用 = 字符来替换最后的 A ,即编码结果为 SGVsbG8hIQ== 。因为 = 字符并不在 Base64 编码索引表中,其意义在于结束符号,在 Base64 解码遇到 = 时即可知道一个 Base64 编码字符串结束。

如果 Base64 编码字符串不会相互拼接再传输,那么最后的 = 也可以省略,解码时如果发现 Base64 编码字符串长度不能被 4 整除,则先补充 = 字符,再解码即可。

解码是对编码的逆向操作,但注意一点:

对于最后的两个 = 字符,转换成两个 A 字符,再转成对应的两个 6 比特二进制 0 值,接着转成原始字符之前,需要将最后的两个 6 比特二进制丢弃,因为它们实际上不携带有效信息。

3. 应用

3.1 HTML 内嵌 Base64 编码图片

绝大多数现在浏览器都支持一种名为 Data URLs 的特性,允许使用 Base64 对图片或其他文件的二进制数据进行编码,将其作为文本字符串嵌入网页中。

Data URLs 格式为: url(data:文件类型;编码方式,编码后的文件内容) 。

当然,也可以直接基于 image 标签嵌入图片。

3.2 MIME

多用途互联网邮件扩展,我们的电子邮件系统,一般使用 SMTP 协议(简单邮件传输协议)将邮件从客户端发往服务器端,邮件客户端使用 POP3 (邮局协议,第三版本) 或 IMAP (交互邮件访问协议)从服务器端获取邮件。

SMTP 协议一开始是基于纯 ASCCII 文本的,对于二进制文件(比如邮件附件中的图像、声音等)的处理并不好,所以后来新增 MIME 标准来编码二进制文件,使其能够通过 SMTP 协议传输。

4. 误区

可能会有人在不理解 Base64 编码的情况下,将其误用于数据加密或数据校验。

Base64 是一种数据编码方式,目的是让数据符合传输协议的要求。标准 Base64 编码解码无需额外信息即可完全可逆,即使你自己自定义字符集设计一种类 Base64 的编码方式用于数据加密,在多数场景下也较容易破解。

对于数据加密应该使用专门的 目前还没有有效方式快速破解的 加密算法。比如:对称加密算法 AES-128-CBC ,对称加密需要密钥,只要密钥没有泄露,通常难以破解;也可以使用非对称加密算法,如 RSA ,利用极大整数因式分解的计算量极大这一特点,使得使用公钥加密的数据,只有使用私钥才能快速解密。

对于数据校验,也应该使用专门的消息认证码生成算法,如 HMAC —— 一种使用单向散列函数构造消息认证码的方法,其过程是不可逆的、唯一确定的,并且使用密钥来生成认证码,其目的是防止数据在传输过程中被篡改或伪造。将原始数据与认证码一起传输,数据接收端将原始数据使用相同密钥和相同算法再次生成认证码,与原有认证码进行比对,校验数据的合法性。

那么针对各大网站被脱库的问题,应该怎么存储用户的登录密码呢?

答案是:在注册时,根据用户设置的登录密码,生成消息认证码,然后存储用户名和消息认证码,不存储原始密码。每次用户登录时,根据登录密码,生成消息认证码,与数据库中存储的消息认证码进行比对,以确认是否为有效用户,这样即使网站被脱库,用户的原始密码也不会泄露,不会为用户使用的其他网站带来账号风险。

当然,使用的消息认证码算法其哈希碰撞的概率应该极低才行,目前一般在 HMAC 算法中使用 SHA256 。对于这种方式需要注意一点:防止用户使用弱密码,否则也可能会被暴力破解。现在的网站一般要求用户密码6个字符以上,并且同时有数字和大小写字母,甚至要求有特殊字符。

另外,也可以使用加入随机 salt 的哈希算法来存储校验用户密码。

5. URL安全

Base64 可以将二进制转码成可见字符方便进行 http 传输,但是 URL 编码器会把标准 Base64 中的 /+ 字符变为形如 %xx 的形式(% 在存入数据库时可能还需要进行转换,比如 ANSI SQL 中已经将其用作通配符),导致数据不一致。

为了解决此问题,可采用一种用于 URL 的改进 Base64 编码,它不仅在末尾去掉填充的 = 号,还将标准 Base64 中的 +/ 分别替换为 -_,解码时再将其替换回去,并在末尾补齐 = 。这样就免去了在 URL 编码器和数据库存储时所要做的转换,避免了编码信息长度在此过程中的增加,并统一了在数据库、表单等地方的对象标识符的格式。

5.1 代码实现

5.1.1 编码

/**
 * URL base64编码
 * '+' => '-'
 * '/' => '_'
 * '=' => ''
 * @param string $string
 */
function url_base64_encode($string) {
    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($string);
}

5.1.2 解码

/**
 * URL base64解码
 * '-' => '+'
 * '_' => '/'
 * 字符串长度%4的余数,补'='
 * @param string $string
 */
function url_base64_decode() {
    $mod4 = strlen($string) % 4;
    if ($mod4) {
        $string .= str_repeat('=', 4 - $mod4);
    }
    return base64_decode(str_replace(['-', '_'], ['+', '/'], $string));
}

5.2 实例

$str = 'Hello!!>?123?<><>你好';
$base64_encode = base64_encode($string);
$base64_decode = base64_decode($base64_encode);
$url_base64_encode = url_base64_encode($string);
$url_base64_decode = url_base64_decode($url_base64_encode);
echo '-----------string: ' . $string . '<br>';
echo '----base64 encode: ' . $base64_encode . '<br>';
echo '----base64 decode: ' . $base64_decode . '<br>';
echo 'url base64 encode: ' . $url_base64_encode . '<br>';
echo 'url base64 decode: ' . $url_base64_decode;

//------ output ------
-----------string: Hello!!>?123?<><>你好
----base64 encode: SGVsbG8hIT4/MTIzPzw+PD7kvaDlpb0=
----base64 decode: Hello!!>?123?<><>你好
url base64 encode: SGVsbG8hIT4_MTIzPzw-PD7kvaDlpb0
url base64 decode: Hello!!>?123?<><>你好

6. 总结

Base64 兼顾字符集大小和编码后数据长度,并且可以灵活替换字符集的最后两个字符,以应对多样的需求,使其适用场景非常广泛。

当然,很多场景下有多种编码方式可选择,并非 Base64 编码不可,视需求,权衡利弊而定。