base64相关介绍

· · 个人记录

本文主要介绍 Base64 的基本信息、编码与解码原理、相关应用以及进一步的改进,将着重介绍 Base64 的编码与解码原理,并在这一部分配有相应的 C++ 代码。

一. Base64 简介

Base64 是网络上最常见的用于传输 8Bit 字节代码的编码方式之一,可用于在 HTTP 环境下传递较长的标识信息。Base64 编码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。

二. Base64 编码与解码原理

(一)编码表

用来把原文本通过 Base64 进行编码。编码表具体如下:

  1. 大写字母:\texttt{A}\texttt{Z}26 个字母,编码从 025

  2. 小写字母:\texttt{a}\texttt{z}26 个字母,编码从 2651

  3. 阿拉伯数字:0910 个数字,编码从 5261

  4. 其它:加号 \texttt{+} 编码为 62,斜杠 \texttt{/} 编码为 63

另外,我们还需要存储每个编码所对应的原来的字符。

这里给出编码表的 C++ 实现代码:

char base[64]; //存储编码表的数组
char table[256]; //存储编码对应的原来的字符
void init() { //预处理出 base[64] 和 table[256] 两个数组
    for(int i = 0; i < 26; i++) base[i] = 'A' + i; //大写字母
    for(int i = 0; i < 26; i++) base[26 + i] = 'a' + i; //小写字母
    for(int i = 0; i < 10; i++) base[52 + i] = '0' + i; //阿拉伯数字
    base[62] = '+', base[63] = '/'; //特殊处理加号和斜杠
    for(int i = 0; i < 256; i++) table[i] = 0xff;
    for(int i = 0; i < 64; i++) table[base[i]] = i; //反向记录编码后的字符对应的 ASCII 代码
    table['='] = 0;
}

(二)编码规则

在了解 Base64 具体的编码原理之前,我们要先了解 \texttt{ASCII} 代码。

例如,大写字母 $\texttt{A}$ 到 $\texttt{Z}$ 的 $\texttt{ASCII}$ 代码分别从 $65$ 到 $90$,小写字母 $\texttt{a}$ 到 $\texttt{z}$ 为 $97$ 到 $122$,阿拉伯数字 $0$ 到 $9$ 对应的代码从 $48$ 到 $57$。也就是说,通常的字符可以和 $\texttt{ASCII}$ 代码建立对应关系。 Base64 编码时,会把原文本中的 $3$ 个字符编码成 $4$ 个字符。具体来说,把原来的连续 $3$ 个字符都转换为 $\texttt{ASCII}$ 代码,并且写成八位二进制的形式(用前导零补齐),变为四个六位二进制数(也用前导零补齐)。 为了方便,下面介绍编码与解码规则时,直接简单地称原文本中的三个字符对应的八位二进制数为 $A_1,A_2,A_3$,编码后的四个六位二进制数为 $B_1,B_2,B_3,B_4$。 编码规则: 1. 取出 $A_1$ 的前六位作为 $B_1$; 2. 取出 $A_1$ 的后两位,和 $A_2$ 的前四位拼接起来,作为 $B_2$; 3. 取出 $A_2$ 的后四位,和 $A_3$ 的前两位拼接起来,作为 $B_3$; 4. 取出 $A_3$ 的后六位作为 $B_4$。 最后需要把 $B_1,B_2,B_3,B_4$ 这四个二进制数,按照 Base64 的编码表,变成四个字符,并加入到新的文本中。 然而,仅仅这样还不够,因为原文本的字符数目可能不是 $3$ 的倍数,这样会导致原文多出 $1$ 或 $2$ 个字符(仍将这些字符对应的 $\texttt{ASCII}$ 代码,分别记为 $A_1$ 和 $A_2$)。我们要对这些多出的字符进行特殊处理: 1. 如果多出一个字符(对应 $A_1$),那么取出 $A_1$ 前六位作为 $B_1$,再取出 $A_1$ 后两位并补上四个零作为 $B_2$,将 $B_1$ 和 $B_2$ 转换为字符,再添加两个等于号($\texttt{==}$)一起加入到新的文本中; 2. 如果多出两个字符(分别对应 $A_1$ 和 $A_2$),那么取出 $A_1$ 的前六位作为 $B_1$,然后取出 $A_1$ 后两位和 $A_2$ 的前四位拼接起来作为 $B_2$,再取出 $A_2$ 的后四位再补两个零作为 $B_3$,将 $B_1,B_2,B_3$ 转换为字符,再添加一个等于号一起加入到新的文本中。 不难发现,经过 base64 编码之后的文本长度比原文本大约多出 $\dfrac{1}{3}$。如果原文本的长度记为 $n$,那么整个编码过程的时间复杂度为 $O(n)$。具体的代码实现过程中可以采用位运算的方式,提高程序运行效率。由于 C++ 会自动将字符类型的变量转变为整数类型,所以不需要在程序中专门转变为 $\texttt{ASCII}$ 代码。 编码的过程可以参考以下代码: ```cpp string encode(string str) { //对str进行编码 string ret; int i; //ret记录编码之后的文本串 for(i = 0; i + 3 <= str.size(); i += 3) { //这一组的三个字符是str[i]、str[i+1]、str[i+2] ret += base[str[i] >> 2]; //str[i]>>2 提取str[i]的后六位 ret += base[(str[i] & 0x03) << 4 | str[i + 1] >> 4]; //str[i]&0x03 提取str[i]的后两位,str[i+1]>>4 提取str[i+1]前四位 ret += base[(str[i + 1] & 0x0f) << 2 | str[i + 2] >> 6]; //str[i+1]&0x0f 提取str[i+1]的后四位,str[i+2]>>6 提取str[i+2]的前两位 ret += base[str[i + 2] & 0x3f]; //str[i+2]&0x3f 提取 str[i+2]的后六位 } if(i < str.size()) { //还有剩余的字符没有编码,即文本长度不为3的倍数 ret += base[str[i] >> 2]; //str[i]>>2 提取str[i]的前六位 if(i + 1 == str.size()) { //剩下一个字符(str[i]) ret += base[(str[i] & 0x03) << 4]; //str[i]&0x03 提取str[i]的后两位,<<4可以在后面补四个零 ret += "=="; //补充两个等号 } else { //剩下两个字符(str[i]和str[i+1]) ret += base[(str[i] & 0x03) << 4 | str[i + 1] >> 4]; //str[i]&0x03 提取str[i]后两位,str[i+1]>>4 提取str[i+1]的前四位 ret += base[(str[i + 1] & 0x0f) << 2]; //str[i+1]&0x0f 提取str[i+1]后四位,<<2用来在后面补两个零 ret += "="; //补充一个等号 } } return ret; //返回编码的结果 } ``` **(三)解码规则** Base64 的解码和编码是互逆的过程。也就是说,对一个文本进行编码,再对加密后的文本进行解码,可以得到原来的文本。 具体来说,要将编码后的文本串,每四个字符一组,还原回原来的三个字符。为了方便,仍然把原文本的三个字符对应的 $\texttt{ASCII}$ 记为 $A_1,A_2,A_3$,把编码后的四个六位二进制数记为 $B_1,B_2,B_3,B_4$。 1. 把 $B_1$ 接上 $B_2$ 的前两位作为 $A_1$; 2. 把 $B_2$ 后四位接上 $B_3$ 的前四位作为 $A_2$; 3. 把 $B_3$ 的后两位接上 $B_4$ 作为 $A_3$。 当然,根据上文的编码规则,$B_1$ 和 $B_2$ 一定不会对应等于号,但是 $B_3$ 和 $B_4$ 可能是等号(还记得吗?这是由于文本长度不一定为 $3$ 的倍数导致的)。 具体来说: 1. 如果 $B_3$ 和 $B_4$ 均为等号,那么 $B_1$ 接上 $B_2$ 的前两位即为 $A_1$,不再有 $A_2$ 和 $A_3$; 2. 如果仅 $B_4$ 为等号,那么 $B_1$ 接上 $B_2$ 的前两位是 $A_1$,$B_2$ 的后四位接上 $B_3$ 的前四位为 $A_2$,不再有 $A_3$。 具体的解码程序如下: ```cpp string decode (string str) { //对str进行解码 string ret; int i; //ret记录解码后的文本串 for (i = 0; i < str.size(); i += 4) { //这一组的四个字符是str[i]到str[i+3] ret += table[str[i]] << 2 | table[str[i + 1]] >> 4; //拼接str[i]和str[i+1]的前两位 if (str[i + 2] != '=') ret += (table[str[i + 1]] & 0x0f) << 4 | table[str[i + 2]] >> 2; //拼接str[i+1]的后四位和str[i+2]的前四位 if (str[i + 3] != '=') ret += (table[str[i + 2]] & 0x03) << 6 | table[str[i + 3]]; //拼接str[i+2]的后两位和str[i+3] } return ret; //返回解码的结果 } ``` 例如,文本 $\texttt{Helloworld}$ 经过编码可以得到 $\texttt{SGVsbG93b3JsZA==}$,编码之后的文本 $\texttt{Y3Nx}$ 对应的原文本为 $\texttt{csq}$,以此类推。 ### **三. Base64 编码的应用** Base64 编码可用于在 HTTP 环境下传递较长的标识信息。例如,在 Java Persistence 系统 Hibernate 中,就采用了 Base64 来将一个较长的唯一标识符(一般为 128-bit 的 UUID)编码为一个字符串,用作 HTTP 表单和 HTTP GET URL 中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在 URL(包括隐藏表单域)中的形式。此时,采用 Base64 编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到。 Base64 还有一些另外的应用,例如: - Mozilla Thunderbird 和 Evolution 用 Base64来保密电子邮件密码 - Base64 也会经常用作一个简单的“加密”来保护某些数据,而真正的加密通常都比较繁琐。 - 垃圾讯息传播者用 Base64 来避过反垃圾邮件工具,因为那些工具通常都不会翻译 Base64 的讯息。 - 在 LDIF 档案,Base64 用作编码字串。 ### **四. Base64 编码的改进** 标准的 Base64 并不适合直接放在 URL 里传输,因为 URL 编码器会把标准 Base64 中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为 ANSI SQL 中已将“%”号用作通配符。 为解决此问题,可采用一种用于 URL 的改进 Base64 编码,它不在末尾填充 “=” 号,并将标准 Base64 中的“+”和“/”分别改成了 “*”和“-”,这样就免去了在 URL 编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。 **参考文献:** 1. 百度百科 $\texttt{ASCII}$ 代码 [https://baike.so.com/doc/7103239-7326232.html](https://baike.so.com/doc/7103239-7326232.html) 2. 百度百科 base64 [https://baike.so.com/doc/5126695-5356001.html](https://baike.so.com/doc/5126695-5356001.html) 3. CSP2021 初赛 S 组 Base64 相关代码及试题(略有改动)