Base64编解码原理

目前Base64已经成为网络上常见的传输8比特字节代码的编码方式之一。在做支付系统时,系统之间的报文交互都需要使用Base64对明文进行转码,然后进行签名或加密,之后再次Base64编码传输。那么,Base64到底起到什么作用呢?

在参数传输的过程中经常遇到的一种情况:使用全英文的没问题,但一旦涉及到中文就会出现乱码情况。与此类似,网络上传输的字符并不全是可打印的字符,比如二进制文件、图片等。Base64的出现就是为了解决此问题,它是基于64个可打印的字符来表示二进制的数据的一种方法。

电子邮件刚问世的时候,只能传输英文,但后来随着用户的增加,中文、日文等文字的用户也有需求,但这些字符并不能被服务器或网关有效处理,因此Base64就登场了。随之,Base64在URL、Cookie、网页传输少量二进制文件中也有相应的使用。

Base64的原理比较简单,Base64定义了"A-Z、a-z、0-9、+、/"64个可打印字符,这是标准的Base64协议规定。在日常使用中我们还会看到=或==号出现在Base64的编码结果中,=在此是作为填充字符出现,待会会讲到。

Base64编码步骤:

  1. 将待转换的字符串按指定的字符集(如UTF-8、GBk等)编码成字节数组
  2. 每三个字节分为一组,每个字节占8比特,那么每组共有24个比特位
  3. 将上面的24个比特位每6个一组,共分为4组
  4. 在每组前面添加两个0,每组由6个变为8个比特位,总共32个比特位,即四个字节
  5. 按照Base64编码表(如下所示)将四个字节转化为四个对应的字符
0 A  17 R   34 i   51 z

1 B  18 S   35 j   52 0

2 C  19 T   36 k   53 1

3 D  20 U   37 l   54 2

4 E  21 V   38 m   55 3

5 F  22 W   39 n   56 4

6 G  23 X   40 o   57 5

7 H  24 Y   41 p   58 6

8 I  25 Z   42 q   59 7

9 J  26 a   43 r   60 8

10 K  27 b   44 s   61 9

11 L  28 c   45 t   62 +

12 M  29 d   46 u   63 /

13 N  30 e   47 v

14 O  31 f   48 w   

15 P  32 g   49 x

16 Q  33 h   50 y

由于Base64是按三个字节来分组,如果最后一组字节数不足三个,那么该如何处理?

  • 两个字节:两个字节共16个比特位,依旧按照规则进行分组。此时总共16个比特位,每6个一组,则第三组缺少2位,低位用0补齐,得到三个Base64编码,第四组完全没有数据则用=补齐
  • 一个字节:一个字节共8个比特位,依旧按照规则进行分组。此时共8个比特位,每6个一组,则第二组缺少4位,低位用0补齐,得到两个Base64编码,而后面两组没有对应数据,都用=补齐

从上面的步骤我们发现:

  • Base64将3个字节共24个比特分成4组,编码后每组对应一个字节,即3个字节编码后变成4个字节,因此Base64编码后要比原文大1/3
  • 为什么使用3个字节一组呢?因为6和8的最小公倍数为24,三个字节正好24个比特位,每6个比特位一组,恰好能够分为4组

注意:

  • 大多数编码都是由字符串转化成二进制,而Base64编码恰恰相反是从二进制转换为字符串
  • Base64编码主要用传输、存储、表示二进制领域,算不上加密,Base64是可以直接解码的
  • 由于Base64是对字节编码,因为同一字符串的不同编码(UTF-8、GBK等)对应的Base64编码不同
  • Base64用6个比特位表示字符,因此称为Base64((2的6次幂为64)。同理,Base32就是用5位,Base16就是用4位

知道了Base64如何编码,那么解码就很容易了,下面说下步骤。

Base64解码步骤:

  1. 按4个字符为一组(Base64编码出来的字符串的长度一定是4的倍数)
  2. 解析拿到每一组有效的24个比特串,如果最后一组有一个=号,则16个比特串有效,最后一组有两个=号,则8个比特串有效(至于是哪些比特串有效,不用再介绍了吧,前面编码步骤中已经介绍过了)
  3. 将有效的比特串转为字节数组,再将字节数组按指定的字符集(如UTF-8、GBk等)转为字符串

参考文章:​​一篇文章彻底弄懂Base64编码原理​​

Java实现Base64编解码(不使用任何API)

下面是纯手工实现的Base64编解码,没有用到任何API(为什么这么说呢?因为强大的Java已经有实现了即class java.util.Base64)

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/**
* Base64编解码实现
*/
public class MyBase64 {
/**
* 编码
*/
public static String encode(String s,String charset) throws UnsupportedEncodingException {
byte[] arr = s.getBytes(charset);
int a = arr.length % 3;
int b = arr.length / 3;
StringBuilder base64Str = new StringBuilder();
// 以3个字节为一组
for (int i = 0; i < 3*b; i=i+3) {
// 3个字节一共24位,每6位为一组,分为4组。由于是6位,一共有2^6=64个值(0~63),因为称为Base64
String bits1 = byteToBinaryString(arr[i]).substring(0, 6);
String bits2 = byteToBinaryString(arr[i]).substring(6, 8)+byteToBinaryString(arr[i+1]).substring(0, 4);
String bits3 = byteToBinaryString(arr[i+1]).substring(4, 8)+byteToBinaryString(arr[i+2]).substring(0, 2);
String bits4 = byteToBinaryString(arr[i+2]).substring(2, 8);
// 计算这4组每一组的值
int w=compute(bits1);
int x=compute(bits2);
int y=compute(bits3);
int z=compute(bits4);
// 将每一组的值转换为Base64编码,并拼接在一起
base64Str.append(convert(w)).append(convert(x)).append(convert(y)).append(convert(z));
}
if(a == 1){
// 分组后,多出来一个字节,共8个bit位,则只能分为两组,即6+2,第三组和第四组直接用=表示,第二组缺失的低四位补0
String bits1 = byteToBinaryString(arr[3*b]).substring(0, 6);
String bits2 = byteToBinaryString(arr[3*b]).substring(6, 8)+"0000";
int w = compute(bits1);
int x = compute(bits2);
base64Str.append(convert(w)).append(convert(x)).append('=').append('=');
}else if(a == 2){
// 分组后,多出来二个字节,共16个bit位,则只能分为三组,即6+6+4,第四组直接用=表示,第二组缺失的低二位补0
String bits1 = byteToBinaryString(arr[3*b]).substring(0, 6);
String bits2 = byteToBinaryString(arr[3*b]).substring(6, 8)+byteToBinaryString(arr[3*b+1]).substring(0, 4);
String bits3 = byteToBinaryString(arr[3*b+1]).substring(4, 8)+"00";
int w = compute(bits1);
int x = compute(bits2);
int y = compute(bits3);
base64Str.append(convert(w)).append(convert(x)).append(convert(y)).append('=');
}
return base64Str.toString();
}

/**
* 解码
*/
public static String decode(String s,String charset) throws UnsupportedEncodingException {
List<Byte> bytes = new ArrayList<>();
// 由于Base64编码是将三个字节作为一组变成4个字节,即4个字符,则解码时按4个字符为一组来进行解码
for (int i = 0; i < s.length(); i=i+4) {
if(s.charAt(i+2) == '='){
// 最后一组有两个=的情况
// 将字符转为Base64编码
byte w = convert(s.charAt(i));
byte x = convert(s.charAt(i+1));
// 获取比特串
String bits = byteToBinaryString(w).substring(2, 8) + byteToBinaryString(x).substring(2, 4);
// 会得到一个字节
for (int j = 0; j < bits.length(); j=j+8) {
bytes.add(binaryStringToByte(bits.substring(j,j+8)));
}
}else if(s.charAt(i+3) == '='){
// 最后一组有一个=的情况
// 将字符转为Base64编码
byte w = convert(s.charAt(i));
byte x = convert(s.charAt(i+1));
byte y = convert(s.charAt(i+2));
// 获取比特串
String bits = byteToBinaryString(w).substring(2, 8) + byteToBinaryString(x).substring(2, 8)+byteToBinaryString(y).substring(2, 6);
// 会得到两个字节
for (int j = 0; j < bits.length(); j=j+8) {
bytes.add(binaryStringToByte(bits.substring(j,j+8)));
}
}else{
// 无=的情况
// 将字符转为Base64编码
byte w = convert(s.charAt(i));
byte x = convert(s.charAt(i+1));
byte y = convert(s.charAt(i+2));
byte z = convert(s.charAt(i+3));
// 获取比特串
String bits = byteToBinaryString(w).substring(2, 8) + byteToBinaryString(x).substring(2, 8)
+ byteToBinaryString(y).substring(2, 8) + byteToBinaryString(z).substring(2, 8);
// 会得到三个字节
for (int j = 0; j < bits.length(); j=j+8) {
bytes.add(binaryStringToByte(bits.substring(j,j+8)));
}
}
}
byte[] arr = new byte[bytes.size()];
for (int i = 0; i < bytes.size(); i++) {
arr[i] = bytes.get(i);
}
// 按指定字符集将byte数组转为字符串
return new String(arr,charset);
}


/**
* 将Base64编码转为对应字符的Ascii编码,进而得到对应字符
* 字符的Ascii编码与Base64编码的映射关系如下:
* System.out.println((int)'A'); //65 0
* System.out.println((int)'a'); //97 26
* System.out.println((int)'0'); //48 52
* System.out.println((int)'+'); //43 62
* System.out.println((int)'/'); //47 63
*/
private static char convert(int w){
if(w >=0 && w <= 25){
return (char)(w+65);
}else if(w >= 26 && w <= 51){
return (char)(w+71);
}else if(w >= 52 && w <= 61){
return (char)(w-4);
}else if(w == 62){
return '+';
}else if(w == 63){
return '/';
}
throw new RuntimeException("convert error");
}

/**
* 将字符转为对应的Ascii编码,然后再转为对应的Base64编码
*/
private static byte convert(char w){
if(w >=65 && w <= 90){
return (byte)(w-65);
}else if(w >= 97 && w <= 122){
return (byte)(w-71);
}else if(w >= 48 && w <= 57){
return (byte)(w+4);
}else if(w == 43){
return 62;
}else if(w == 47){
return 63;
}
throw new RuntimeException("convert error");
}

/**
* 计算任意位数二进制比特串的十进制的值
*/
private static int compute(String bits){
int sum = 0;
for (int j = 0; j < bits.length(); j++) {
if(bits.charAt(j) == '1'){
sum+= (int)Math.pow(2,bits.length()-1-j);
}
}
return sum;
}

/**
* byte转8位比特串
*/
private static String byteToBinaryString(byte b){
// -128特殊处理
if(b == -128){
return "10000000";
}
byte tmp = b;
if(tmp<0){
// 将符号位从1转为0。如-127的二进制为10000001,只需要用乘积取余法得到1的比特串,再将高位0变成1就得到-127的比特串
tmp = (byte)(tmp & 127);
}
// 存储8个比特位
int index = 7;
byte[] bits = new byte[8];
// 开始转换
byte x = tmp;
do {
byte y = (byte) (x%2);
x = (byte) (x >> 1);
bits[index--] = y;
}while (x != 0);
if(b < 0){
// 如果是负数,则高位的0变为1
bits[0] = 1;
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < bits.length; i++) {
sb.append(bits[i]);
}
return sb.toString();
}

/**
* 8位比特串转byte
*/
private static byte binaryStringToByte(String bits){
if(bits.length() != 8){
throw new RuntimeException("binaryStringToByte error");
}
if(bits.charAt(0) == '0'){
return (byte) compute(bits);
}else if(bits.charAt(0) == '1'){
if(bits.equals("10000000")){
return -128;
}else{
// 计算负数补码的值
byte v1 = (byte)compute(bits.substring(1));
// 计算负数原码的值
byte v2 = (byte)(~(v1-1));
int v3 = compute(byteToBinaryString(v2).substring(1));
return (byte)-v3;
}
}
throw new RuntimeException("binaryStringToByte error");
}

public static void main(String[] args) throws UnsupportedEncodingException {
System.out.println(encode("你不要过来啊?!ok","UTF-8"));
System.out.println(decode("5L2g5LiN6KaB6L+H5p2l5ZWKPyFvaw==","UTF-8"));
}
}

运行结果如下所示:

Base64编解码原理并用Java手工实现Base64编解码_java