Base64编解码原理
目前Base64已经成为网络上常见的传输8比特字节代码的编码方式之一。在做支付系统时,系统之间的报文交互都需要使用Base64对明文进行转码,然后进行签名或加密,之后再次Base64编码传输。那么,Base64到底起到什么作用呢?
在参数传输的过程中经常遇到的一种情况:使用全英文的没问题,但一旦涉及到中文就会出现乱码情况。与此类似,网络上传输的字符并不全是可打印的字符,比如二进制文件、图片等。Base64的出现就是为了解决此问题,它是基于64个可打印的字符来表示二进制的数据的一种方法。
电子邮件刚问世的时候,只能传输英文,但后来随着用户的增加,中文、日文等文字的用户也有需求,但这些字符并不能被服务器或网关有效处理,因此Base64就登场了。随之,Base64在URL、Cookie、网页传输少量二进制文件中也有相应的使用。
Base64的原理比较简单,Base64定义了"A-Z、a-z、0-9、+、/"64个可打印字符,这是标准的Base64协议规定。在日常使用中我们还会看到=或==号出现在Base64的编码结果中,=在此是作为填充字符出现,待会会讲到。
Base64编码步骤:
- 将待转换的字符串按指定的字符集(如UTF-8、GBk等)编码成字节数组
- 每三个字节分为一组,每个字节占8比特,那么每组共有24个比特位
- 将上面的24个比特位每6个一组,共分为4组
- 在每组前面添加两个0,每组由6个变为8个比特位,总共32个比特位,即四个字节
- 按照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解码步骤:
- 按4个字符为一组(Base64编码出来的字符串的长度一定是4的倍数)
- 解析拿到每一组有效的24个比特串,如果最后一组有一个=号,则16个比特串有效,最后一组有两个=号,则8个比特串有效(至于是哪些比特串有效,不用再介绍了吧,前面编码步骤中已经介绍过了)
- 将有效的比特串转为字节数组,再将字节数组按指定的字符集(如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"));
}
}
运行结果如下所示: