1 什么是Base64
在以前
,email只支持ASCII
编码的字符,而ASCII
只能表示128个字符。若一封email存在非ASCII
编码的字符,当它通过网关的时候,网关(以前
)会对email的二进制位作调整,把非ASCII编码的8位二进制码的最高位设置为0,因此,接收方收到的email会出现乱码。
如何使一封包含非ASCII
编码字符的email正确地发送到目的地呢?Base64应运而生,它定义了规则映射表
,发送方
按规则映射表
把内容编码成只包含ASCII字符
的字符串,接收方
收到信息后,根据规则映射表
对信息进行解码还原。
2 规则映射表
在RFC(Request For Comments) 2045文档中规定了Base64的映射规则:
十进制 | 编码 | 十进制 | 编码 |
---|---|---|---|
0 | A | 32 | g |
1 | B | 33 | h |
2 | C | 34 | i |
3 | D | 35 | j |
4 | E | 36 | k |
5 | F | 37 | l |
6 | G | 38 | m |
7 | H | 39 | n |
8 | I | 40 | o |
9 | J | 41 | p |
10 | K | 42 | q |
11 | L | 43 | r |
12 | M | 44 | s |
13 | N | 45 | t |
14 | O | 46 | u |
15 | P | 47 | v |
16 | Q | 48 | w |
17 | R | 49 | x |
18 | S | 50 | y |
19 | T | 51 | z |
20 | U | 52 | 0 |
21 | V | 53 | 1 |
22 | W | 54 | 2 |
23 | X | 55 | 3 |
24 | Y | 56 | 4 |
25 | Z | 57 | 5 |
26 | a | 58 | 6 |
27 | b | 59 | 7 |
28 | c | 60 | 8 |
29 | d | 61 | 9 |
30 | e | 62 | + |
31 | f | 63 | / |
(pad) | = |
3 Base64与加密
严格来说,Base64并不是一种加密方式,而是一种使用了表置换算法
编码方式。由于Base64的规则映射表
是公开的,因此,我们不能把一些私密的信息经Base64简单编码后就在网络上传播,例如密码、私钥。
Base64编码把二进制流编码成可见的字符,方便一些文件的保存和传输,例如:
x509
证书,有der
和pem
两种格式,der
是二进制格式,pem
则是der
经Base64编码后的格式。假设,我们希望把一个证书拷贝到远程的服务器,正好,服务器没有提供sftp服务,对于pem,我们只需要打开文件,复制文件中的内容,并拷贝到服务器就可以了,对于der,我们则无法这样拷贝,因为,使用文本编辑器打开der后,会显示乱码。
4 Base64的编码过程
Base64的编码主要经过以下步骤:
(1) 把要进行Base64编码的二进制码进行分组,每3个字节
,即每24位
一组,若二进制码的字节数无法被3整除,则要低位补 (3 - 二进制码字节数 mod 3) * 8
位0,例如:如果要编码的内容为11个字节,那么则要在低位补8位0,根据 (3 - 11 mod 3) * 8
计算得出。
(2) 经过第一步后,再把24位的分组继续拆成6位一个分组。
(3) 现在每个分组为6位,然后,每个分组的高位补2位0。
(4) 最后,把第三步处理后得到的二进制码,逐个字节从规则对照表
找到对应的编码。
下面展示了把"great中国"进行Base64的编码过程:
下面的代码段可以获取"great中国"的UTF-8的二进制码
byte[] bytes = "great中国".getBytes(Charset.forName("UTF-8"));
for (int i = 0; i < bytes.length; i++) {
if (i > 0) {
System.out.print("_");
}
System.out.print(String.format(
"%8s",
Integer.toBinaryString(bytes[i] & 0xff)).replaceAll(" ", "0")
);
}
运行上面的代码段后,我们获取字符串的二进制码为:
01100111
_01110010
_01100101
_01100001
_01110100
_11100100
_10111000
_10101101
_11100101
_10011011
_10111101
共11个字节。
按24位(3个字节)1组的规则对上面的二进制码进行分组
(01100111_01110010_01100101)
_(01100001_01110100_11100100)
_(10111000_10101101_11100101)
_(10011011_10111101)
共分 Ceil(11字节 / 3个字节) = 4
组。
低位补0
上面进行分组后,最后的分组不足3个字节,所以低位需要补8位0。
(01100111_01110010_01100101)
_(01100001_01110100_11100100)
_(10111000_10101101_11100101)
_(10011011_10111101_
00000000)
按每6位为一组再分组
(011001)
_(110111)
_(001001)
_(100101)
_(011000)
_(010111)
_(010011)
_(100100)
_(101110)
_(001010)
_(110111)
_(100101)
_(100110)
_(111011)
_(110100)
_(000000)
每个分组的高位补2位0
(
00011001)
_(
00110111)
_(
00001001)
_(
00100101)
_(
00011000)
_(
00010111)
_(
00010011)
_(
00100100)
_(
00101110)
_(
00001010)
_(
00110111)
_(
00100101)
_(
00100110)
_(
00111011)
_(
00110100)
_(
00000000)
把分组转成十进制并在对照表找出对应的编码
二进制 | 十进制 | 规则对照表的编码 |
---|---|---|
00011001 | 25 | Z |
00110111 | 55 | 3 |
00001001 | 9 | J |
00100101 | 37 | l |
00011000 | 24 | Y |
00010111 | 23 | X |
00010011 | 19 | T |
00100100 | 36 | k |
00101110 | 46 | u |
00001010 | 10 | K |
00110111 | 55 | 3 |
00100101 | 37 | l |
00100110 | 38 | m |
00111011 | 59 | 7 |
00110100 | 52 | 0 |
00000000 | 第一步分组,要求每个组为3个字节,由于最后一个分组不足3个字节,所以补了0用于填充 | = |
最终得到"great中国"的Base64编码为Z3JlYXTkuK3lm70=
。
使用bouncycastle的Base64的工具类对"great中国"进行编码
我们可以看到结果跟我们上面手动编码的结果是一致的。
5 Base64编码的特点
(1) Base64编码的位长度一定能被32整除。
(2) 原文长度为编码后长度的3/4
,因为,每6位的分组的高位补了2位0。
(3) Base64的编码结尾最多会有两个=号,因为,若二进制码的字节数无法被3整除,则要低位补 (3 - 二进制码字节数 mod 3) * 8
位0,即用=号填充。
6 用Java实现Base64编码
上面比较直观地描述了Base64的编码过程,下面将尝试使用Java实现Base64的编码过程。
6.1 规则映射表
使用一个长度为64的byte
数组作为编码规则表,其中,数组的下标为十进制
值,元素为编码
。
private final static byte[] encodingArray =
{
(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
(byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
(byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b',
(byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i',
(byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p',
(byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w',
(byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+',
(byte) '/'
};
除了64个编码外,还需要定义用于填充的=
号。
private final static byte padding = (byte)'=';
6.2 计算长度
开始编码前,需要把字节数组
拆成每3个字节(24位)
一组进,因此,按字节数组长度
mod 3
得需填充处理的字节长度
,结果为1
或2
;字节数组长度
- 需填充处理的字节长度
得优先处理的字节长度
。
// 计算不足3个字节需要做填充的字节长度,例如,长度为11,则有2个字节要填充处理
int tailLength = bytes.length % 3;
// 计算能被3整除的长度,例如,长度为11,则先处理前9个字节
int processLength = bytes.length - tailLength;
6.3 处理优先字节
先对前processLength
个字节进行分组处理。上面手动编码的过程是,每3个字节
一组,然后再分为6位
一小组,然后每个小组高位补2位0,最终,每个小组为1个字节,通过字节的数值在规则映射表
中找出编码。
若在程序中,我们可以借助位运算的技巧简单实现这个过程,例如,分组如下:
01100111
_01110010
_01100101
任务是要把上面的3个字节分为4组。
先把这3个字节转int类型
我们先把组里的3个字节转换为int类型,分别把这三个字段与0xff
做与运算。
int ele1 = bytes[i] & 0xff;
int ele2 = bytes[i + 1] & 0xff;
int ele3 = bytes[i + 2] & 0xff;
跟0xff
做与运算的目的是,保证原来字节的8位在转换成int后不会被改变
,因为,若字节的首位是1,即负数,那么转换int的时候,补码形式
会使原来字节的8位发生改变,而与0xff
做与运算则只是在原来8位前面补0
。
转换int后,
ele1
为: 00000000
_00000000
_00000000
_01100111
ele2
为: 00000000
_00000000
_00000000
_01110010
ele3
为: 00000000
_00000000
_00000000
_01100101
下面对这3个int分成4个小组。
第1小组
第1小组取ele1
最后一个字节的前6位
,因此,把ele1无符号右移2位
。
encodedBytes[encodedIdx++] = encodingArray[(ele1 >>> 2) & 0x3f];
上面的代码中,跟0x3f
做按位与得目的是,只保留最后的6位
,其他位都为0。
ele1无符号右移2位
后的二进制码为:
00000000
_00000000
_00000000
_0011001
以(ele1 >>> 2) & 0x3f
的数值作为下标立刻就从上面定义的规则映射数组encodingArray
中获取第1小组的编码了。
第2小组
第1小组取ele1
最后1个字节的后2位
和ele2
最后1个字节的前4位
组成,即由下面粗体的位组成:
ele1
: 00000000
_00000000
_00000000
_011001
11
ele2
: 00000000
_00000000
_00000000
_01110010
(1) 先对ele1
左移动4位
00000000
_00000000
_00000110
_01
110000
(2) 对ele2
无符号右移4位
00000000
_00000000
_00000000
_0000
0111
(3) 上面结果按位与
00000000
_00000000
_00000110
_01
110000
&00000000
_00000000
_00000000
_0000
0111
得
00000000
_00000000
_00000110
_01110111
(4) 跟0x3f
按位与,只保留最后6位
00000000
_00000000
_00000110
_01110111
&00000000
_00000000
_00000000
_00111111
得
00000000
_00000000
_00000000
_00110111
整个过程的代码如下:
encodedBytes[encodedIdx++] = encodingArray[((ele1 << 4) | (ele2 >>> 4)) & 0x3f];
以得出的数值作为下标立刻就从上面定义的规则映射数组encodingArray
中获取第2小组的编码了。
第3个小组
第3小组取ele2
最后1个字节的后4位
和ele3
最后1个字节的前2位
组成,即由下面粗体的位组成:
ele2
: 00000000
_00000000
_00000000
_0111
0010
ele3
: 00000000
_00000000
_00000000
_01100101
(1) 先对ele2
左移动2位
00000000
_00000000
_00000001
_11
001000
(2) 对ele3
无符号右移6位
00000000
_00000000
_00000000
_000000
01
(3) 上面结果按位与
00000000
_00000000
_00000001
_11
001000
&00000000
_00000000
_00000000
_000000
01
得
00000000
_00000000
_00000000
_00
001001
(4) 跟0x3f
按位与,只保留最后6位
00000000
_00000000
_00000000
_00
001001
&00000000
_00000000
_00000000
_00111111
得
00000000
_00000000
_00000000
_00
001001
整个过程的代码如下:
encodedBytes[encodedIdx++] = encodingArray[((ele2 << 2) | (ele3 >>> 6)) & 0x3f];
以得出的数值作为下标立刻就从上面定义的规则映射数组encodingArray
中获取第3小组的编码了。
第4小组
第4小组取ele3
最后1个字节的后6位
:
ele3
: 00000000
_00000000
_00000000
_01
100101
直接与0x3f
做按位与。
代码如下:
encodedBytes[encodedIdx++] = encodingArray[ele3 & 0x3f];
以得出的数值作为下标立刻就从上面定义的规则映射数组encodingArray
中获取第4小组的编码了。
至此,该包含3个字节
的小组经过处理变成了4个字节
。
6.4 填充
6.5 完整代码
public class Base64Encoder {
// Base64的规则映射表
// 数组 下标 对应 编码
private final static byte[] encodingArray =
{
(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
(byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
(byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b',
(byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i',
(byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p',
(byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w',
(byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+',
(byte) '/'
};
// 填充用的=
private final static byte padding = (byte)'=';
public static byte[] encode(byte[] bytes) {
// 计算尾数,后面填充用,例如,长度为11,则有2个字节要填充处理
int tailLength = bytes.length % 3;
// 计算能被3整除的长度,例如,长度为11,则先处理前9个字节
int processLength = bytes.length - tailLength;
int ele1, ele2, ele3;
byte[] encodedBytes = new byte[4 * processLength / 3 + (tailLength == 0 ? 0 :4) ];
int encodedIdx = 0;
// 3个字节为一组,因此循环步长为3
for (int i = 0; i < processLength; i += 3)
{
ele1 = bytes[i] & 0xff;
ele2 = bytes[i + 1] & 0xff;
ele3 = bytes[i + 2] & 0xff;
// 分组第1个成员,为ele1的前6位
encodedBytes[encodedIdx++] = encodingArray[(ele1 >>> 2) & 0x3f];
// 第2个成员,为ele1的后2位和ele2的前4位
encodedBytes[encodedIdx++] = encodingArray[((ele1 << 4) | (ele2 >>> 4)) & 0x3f];
// 第3个成员,为ele2的前4位和ele3的前2位
encodedBytes[encodedIdx++] = encodingArray[((ele2 << 2) | (ele3 >>> 6)) & 0x3f];
// 第4个成员,为ele3的后6位
encodedBytes[encodedIdx++] = encodingArray[ele3 & 0x3f];
}
/* 更新尾部,tailLength的值会是1或2,0则不处理 */
int tailEle1, tailEle2;
// 当剩下1个字节未处理
if (tailLength == 1) {
// 最后1个未处理的字节
tailEle1 = bytes[processLength] & 0xff;
// 组第1个成员为tailEle1的前6位
encodedBytes[encodedIdx++] = encodingArray[(tailEle1 >>> 2) & 0x3f];
// 组第2个成员为这样1个字节,前2位为tailEle1的后2,其余位为0
encodedBytes[encodedIdx++] = encodingArray[(tailEle1 << 4) & 0x3f];
// 补2个=作填充
encodedBytes[encodedIdx++] = padding;
encodedBytes[encodedIdx++] = padding;
}
// 剩余2个字节未处理
else if (tailLength == 2) {
tailEle1 = bytes[processLength] & 0xff;
tailEle2 = bytes[processLength + 1] & 0xff;
// 组第1个成员为tailEle1的前6位
encodedBytes[encodedIdx++] = encodingArray[(tailEle1 >>> 2) & 0x3f];
// 组第2个成员为这样1个字节,前2位为tailEle1的后2,后4位为tailEle2的前4位
encodedBytes[encodedIdx++] = encodingArray[((tailEle1 << 4) | (tailEle2 >>> 4)) & 0x3f];
// 组第3个成员为这样1个字节,前
encodedBytes[encodedIdx++] = encodingArray[(tailEle2 << 2) & 0x3f];
// 补1个=作填充
encodedBytes[encodedIdx++] = padding;
}
return encodedBytes;
}
}