注册

Base64编码解码原理

Base64编码与解码

原理涉及的算法

1、短除法

短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。

实例:以字符n对应的ascII编码110为例。

110 / 2  = 55...0
55 / 2 = 27...1
27 / 2 = 13...1
13 / 2 = 6...1
6   / 2 = 3...0
3   / 2 = 1...1
1   / 2 = 0...1

将余数从下到上进行排列组合,得到字符n对应的ascII编码110转二进制为1101110,因为一字节对应8位(bit), 所以需要向前补0补足8位,得到01101110。其余字符同理可得。

2、按权展开求和

按权展开求和, 8位二进制数从右到左,次数是0到7依次递增, 基数*底数次数,从左到右依次累加,相加结果为对应十进制数。我们已二进制数01101110转10进制为例:

(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27(01101110)_2 = 0 * 2^0 + 1 * 2 ^ 1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 + 1 * 2^6 + 0 * 2^7(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27

3、位概念

二进制数系统中,每个0或1就是一个位(bit,比特),也叫存储单元,位是数据存储的最小单位。其中 8bit 就称为一个字节(Byte)。

4、移位运算符

移位运算符在程序设计中,是位操作运算符的一种。移位运算符可以在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。在base64的编码和解码过程中操作的是正数,所以仅使用<<(左移)、>>(带符号右移)两种运算符。

  1. 左移运算:是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。【左移相当于一个数乘以2的次方】

  2. 右移运算:是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1。【右移相当于一个数除以2的次方】

// 左移
01101000 << 2 -> 101000(左侧移出位被丢弃) -> 10100000(右侧空位一律补0)
// 右移
01101000 >> 2 -> 011010(右侧移出位被丢弃) -> 00011010(左侧空位一律补0)

5、与运算、或运算

与运算、或运算都是计算机中一种基本的逻辑运算方式。

  1. 与运算:符号表示为&。运算规则:两位同时为“1”,结果才为“1”,否则为0

  2. 或运算:符号表示为|。运算规则:两位只要有一位为“1”,结果就为“1”,否则为0

什么是base64编码

2^6=64\

\

Base64编码是将字符串以每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节(6比特有效字节,最左边两个永远为0,其实也是8比特的字节)子序列,再将得到的子序列查找Base64的编码索引表,得到对应的字符拼接成新的字符串的一种编码方式。

每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节的拆分过程如下图所示:

0f59ed586e8e4fcb84f7763520df7654~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

为什么base64编码后的大小是原来的4/3倍

因为6和8的最大公倍数是24,所以3个8比特的字节刚好可以拆分成4个6比特的字节,3 x 8 = 6 x 4。计算机中,因为一个字节需要8个存储单元存储,所以我们要把6个比特往前面补两位0,补足8个比特。如下图所示:

87b1330cefca4831b21d4df05ff0aadb~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

补足后所需的存储单元为32个,是原来所需的24个的4/3倍。这也就是base64编码后的大小是原来的4/3倍的原因。

为什么命名为base64呢?

因为6位(bit)的二进制数有2的6次方个,也就是二进制数(00000000-00111111)之间的代表0-63的64个二进制数。

不是说一个字节是用8位二进制表示的吗,为什么不是2的8次方?

因为我们得到的8位二进制数的前两位永远是0,真正的有效位只有6位,所以我们所能够得到的二进制数只有2的6次方个。

Base64字符是哪64个?

Base64的编码索引表,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符来代表(00000000-00111111)这64个二进制数。即

let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

编码原理

要把3个字节拆分成4个字节可以怎么做?

流程图

b24e9748ebdd44d48880e16cc1704c46~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

思路

分析映射关系:abc → xyzi。我们从高位到低位添加索引来分析这个过程

  • x: (前面补两个0)a的前六位 => 00a7a6a5a4a3a2

  • y: (前面补两个0)a的后两位 + b的前四位 => 00a1a0b7b6b5b4

  • z: (前面补两个0)b的后四位 + c的前两位 => 00b3b2b1b0c7c6

  • i: (前面补两个0)c的后六位 => 00c5c4c3c2c1c0

通过上述的映射关系,得到实现思路:

  1. 将字符对应的AscII编码转为8位二进制数

  2. 将每三个8位二进制数进行以下操作

    • 将第一个数右移位2位,得到第一个6位有效位二进制数

    • 将第一个数 & 0x3之后左移位4位,得到第二个6位有效位二进制数的第一个和第二个有效位,将第二个数 & 0xf0之后右移位4位,得到第二个6位有效位二进制数的后四位有效位,两者取且得到第二个6位有效位二进制

    • 将第二个数 & 0xf之后左移位2位,得到第三个6位有效位二进制数的前四位有效位,将第三个数 & 0xC0之后右移位6位,得到第三个6位有效位二进制数的后两位有效位,两者取且得到第三个6位有效位二进制

    • 将第三个数 & 0x3f,得到第四个6位有效位二进制数

  3. 将获得的6位有效位二进制数转十进制,查找对呀base64字符

代码实现

以hao字符串为例,观察base64编码的过程,将上面转换通过代码逻辑分析实现

// 输入字符串
let str = 'hao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4, out
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(0) & 0xff // 104 01101000
char2 = str.charCodeAt(1) & 0xff // 97 01100001
char3 = str.charCodeAt(2) & 0xff // 111 01101111
// 输出6位有效字节二进制数
out1 = char1 >> 2 // 26 011010
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
out4 = char3 & 0x3f // 47 101111

out = base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv

算法剖析

  1. out1: char1 >> 2

    01101000 -> 00011010
    复制代码
  2. out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4

    // 且运算
    01101000       01100001
    00000011       11110000
    --------       --------
    00000000       01100000

    // 移位运算后得
    00000000       00000110

    // 或运算
    00000000
    00000110
    --------
    00000110
    复制代码

第三个字符第四个字符同理

整理上述代码,扩展至多字符字符串

// 输入字符串
let str = 'haohaohao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
  // 定义输入、输出字节的二进制数
  let char1, char2, char3, out1, out2, out3, out4
  // 将字符对应的ascII编码转为8位二进制数
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  char3 = str.charCodeAt(index++) & 0xff // 111 01101111
  // 输出6位有效字节二进制数
  out1 = char1 >> 2 // 26 011010
  out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
  out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
  out4 = char3 & 0x3f // 47 101111

  out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv
}

原字符串长度不是3的整倍数的情况,需要特殊处理

    ...
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  if (index == len) {
      out2 = (char1 & 0x3) << 4
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
      return out
  }
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  if (index == len) {
      out1 = char1 >> 2 // 26 011010
      out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
      out3 = (char2 & 0xf) << 2
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
      return out
  }
  ...

全部代码

function base64Encode(str) {
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(index++) & 0xff
out1 = char1 >> 2
if (index == len) {
out2 = (char1 & 0x3) << 4
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
return out
}
char2 = str.charCodeAt(index++) & 0xff
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4
if (index == len) {
out3 = (char2 & 0xf) << 2
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
return out
}
char3 = str.charCodeAt(index++) & 0xff
// 输出6位有效字节二进制数
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6
out4 = char3 & 0x3f

out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4]
}
return out
}
base64Encode('haohao') // aGFvaGFv
base64Encode('haoha') // aGFvaGE=
base64Encode('haoh') // aGFvaA==

解码原理

逆向推导,由每4个6位有效位的二进制数合并成3个8位二进制数,根据ascII编码映射到对应字符后拼接字符串

思路

分析映射关系 xyzi -> abc

  • a: x后六位 + y第三、四位 => x5x4x3x2x1x0y5y4

  • b: y后四位 + z第三、四、五、六位 => y3y2y1y0z5z4z3z2

  • c: z后两位 + i后六位 => z1z0i5i4i3i2i1i0

  1. 将字符对应的base64字符集的索引转为6位有效位二进制数

  2. 将每四个6位有效位二进制数进行以下操作

    1. 第一个二进制数左移位2位,得到新二进制数的前6位,第二个二进制数 & 0x30之后右移位4位,取或集得到第一个新二进制数

    2. 第二个二进制数 & 0xf之后左移位4位,第三个二进制数 & 0x3c之后右移位2位,取或集得到第二个新二进制数

    3. 第二个二进制数 & 0x3之后左移位6位,与第四个二进制数取或集得到第二个新二进制数

  3. 根据ascII编码映射到对应字符后拼接字符串

代码实现

// base64字符串
let str = 'aGFv'
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
// 获取索引值
let char1 = base64CharsArr.findIndex(char => char==str[0]) & 0xff // 26 011010
let char2 = base64CharsArr.findIndex(char => char==str[1]) & 0xff // 6 000110
let char3 = base64CharsArr.findIndex(char => char==str[2]) & 0xff // 5 000101
let char4 = base64CharsArr.findIndex(char => char==str[3]) & 0xff // 47 101111
let out1, out2, out3, out
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)

遇到有用'='补过位的情况时

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let char1 = base64CharsArr.findIndex(char => char==str[0])
let char2 = base64CharsArr.findIndex(char => char==str[1])
let out1, out2, out3, out
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[2])
// 第三位不在base64对照表中时,只拼接第一个字符串
if (char3 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out = String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[3])
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
if (char4 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out = String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
return out
}

解码整个字符串,整理代码后

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let i = 0
let len = str.length
let out = ''
while(i < len) {
let char1 = base64CharsArr.findIndex(char => char==str[i])
i++
let char2 = base64CharsArr.findIndex(char => char==str[i])
i++
let out1, out2, out3
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个字符串
out1 = char1 << 2 | (char2 & 0x30) >> 4
if (char3 == -1) {
out = out + String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
if (char4 == -1) {
out = out + String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = out + String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
}
return out
}
base64decode('aGFvaGFv') // haohao
base64decode('aGFvaGE=') // haoha
base64decode('aGFvaA==') // haoh

上述解码核心是字符与base64字符集索引的映射,网上看到过使用AscII编码索引映射base64字符索引的方法

let base64DecodeChars = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
//
let char1 = 'hao'.charCodeAt(0) // h -> 104
base64DecodeChars[char1] // 33 -> base64编码表中的h

由此可见,base64DecodeChars对照accII编码表的索引存放的是base64编码表的对应字符的索引。

jdk1.8之前的方式

Base64编码与解码时,会使用到JDK里sun.misc包套件下的BASE64Encoder类和BASE64Decoder类

sun.misc包所提供的Base64编码解码功能效率不高,因此在1.8之后的jdk版本已经被删除了

// 编码器
final BASE64Encoder encoder = new BASE64Encoder();
// 解码器
final BASE64Decoder decoder = new BASE64Decoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = encoder.encode(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decodeBuffer(encodedText), "UTF-8"));

Apache Commons Codec包的方式

Apache Commons Codec 有提供Base64的编码与解码功能,会使用到 org.apache.commons.codec.binary 套件下的Base64类别,用法如下

1、引入依赖

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-compress</artifactId>
  <version>1.21</version>
</dependency>

2、代码实现

final Base64 base64 = new Base64();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = base64.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(base64.decode(encodedText), "UTF-8"));

jdk1.8之后的方式

与sun.misc包和Apache Commons Codec所提供的Base64编解码器方式来比较,Java 8提供的Base64拥有更好的效能。实际测试编码与解码速度,Java 8提供的Base64,要比 sun.misc 套件提供的还要快至少11倍,比 Apache Commons Codec 提供的还要快至少3倍。

// 解码器
final Base64.Decoder decoder = Base64.getDecoder();
// 编码器
final Base64.Encoder encoder = Base64.getEncoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes(StandardCharsets.UTF_8);
//编码
final String encodedText = encoder.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decode(encodedText), StandardCharsets.UTF_8));

总结

Base64 是一种数据编码方式,可做简单加密使用,可以t通过改变base64编码映射顺序来形成自己独特的加密算法进行加密解密。

编码表

Base64编码表

cb595cfa0f384eac8584c1ea48bae05e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

AscII码编码表

image-20220522121115127.png

作者:loginfo
来源:juejin.cn/post/7100421228644532255

0 个评论

要回复文章请先登录注册