Base64编码原理解析与Java实现
20180202更新:简化前言,重新排版。使用MARKDOWN排版:新版链接
一、前言
碰巧在慕课看到一个关于Base64的课程《Java实现Base64加密》,发现介绍了3个API,而非个人感兴趣的实现原理。犟劲儿上来就想造轮子,于是抱着试试的心态,查了官网RFC2045(下载完整PDF),随后有了本文。
二、Base64是什么
以下摘自Base64的维基百科:
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。它可用来作为电子邮件的传输编码。在Base64中的可打印字符包括字母A-Z、a-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。一些如uuencode的其他编码方法,和之后binhex的版本使用不同的64字符集来代表6个二进制数字,但是它们不叫Base64。 Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据。包括MIME的email、在XML中存储复杂数据。
三、规则与原理
- 原理
在MIME格式的电子邮件中,base64可以用来将binary的字节序列数据编码成ASCII字符序列构成的文本。使用时,在传输编码方式中指定base64。使用的字符包括大小写字母各26个,加上10个数字,和加号“+”,斜杠“/”,一共64个字符,等号“=”用来作为后缀用途。 完整的base64定义可见RFC1421和RFC2045。编码后的数据比原始数据略长,为原来的4/3。在电子邮件中,根据RFC822规定,每76个字符,还需要加上一个回车换行。可以估算编码后数据长度大约为原长的135.1%。 转换的时候,将三个byte的数据,先后放入一个24bit的缓冲区中,先来的byte占高位。数据不足3byte的话,于缓冲器中剩下的bit用0补足。然后,每次取出6(因为2^6=64)个bit,按照其值选择ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/中的字符作为编码后的输出。不断进行,直到全部输入数据转换完成。 当原数据长度不是3的整数倍时,如果最后剩下一个输入数据,在编码结果后加2个“=”;如果最后剩下两个输入数据,编码结果后加1个“=”;如果没有剩下任何数据,就什么都不要加,这样才可以保证数据还原的正确性。
上面提到编码后的数据长度约为原长的135.1%,计算方式:1*(4/3)*((76+1)/76)≈1.351
个人理解总结,得到以下3点:
- 将原始数据以先后顺序每3个字节分成一组*。在每一组中,把3段8bit的字节(3*8=24)按高低位顺序分为每段6bit的4段(4*6=24),将每段转为十进制得到0~63之间的数,以之为码值/索引在Base64编码表中对照/映射得到4个密文。重复以上操作。
- 密文每76个字符数据后加一个换行符*。
- 若原始数据字节长度除3余1,则在输出数据末尾加2个“=”;若原始数据长度除3余2,则在输出数据末尾加1个“=”。
*注意这里的换行符指CRLF( ),而不是LF( ),为什么?请看RFC2045(下载完整PDF)
- 规则
↑ ASCII编码表↑
↑Base64编码表↑
四、示例
根据原数据的长度向3求余可得3种情况(示例代码下载):
- 向3求余得0
- 向3求余得1
- 向3求余得2
例1 假设我们的明文为“Base64”(数据长度为6,6%3=0),则其编码计算方式如下:
例2 假设我们的明文为“test”(数据长度为4,4%3=1),则其编码计算方式如下:
例3 假设我们的明文为“JiaMi”(数据长度为5,5%3=2),则其编码计算方式如下:
五、编码思路
代码注释充分,思路不述。
六、代码
package com.dokio.base64; public class Base64 { //Constructor public Base64() { } private static final String base64Code= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; public static String encode(String srcStr) { //有效值检查 if(srcStr == null || srcStr.length() == 0) { return srcStr; } //将明文的ASCII码转为二进制位字串 char[] srcStrCh= srcStr.toCharArray(); StringBuilder asciiBinStrB= new StringBuilder(); String asciiBin= null; for(int i= 0; i< srcStrCh.length; i++) { asciiBin= Integer.toBinaryString((int)srcStrCh[i]); while(asciiBin.length()< 8) { asciiBin= "0"+ asciiBin; } asciiBinStrB.append(asciiBin); } //跟据明文长度在二进制位字串尾部补“0” while(asciiBinStrB.length()% 6!= 0) { asciiBinStrB.append("0"); } String asciiBinStr= String.valueOf(asciiBinStrB); //将上面得到的二进制位字串转为Value,再跟据Base64编码表将之转为Encoding char[] codeCh= new char[asciiBinStr.length()/ 6]; int index= 0; for(int i= 0; i< codeCh.length; i++) { index= Integer.parseInt(asciiBinStr.substring(0, 6), 2); asciiBinStr= asciiBinStr.substring(6); codeCh[i]= base64Code.charAt(index); } StringBuilder code= new StringBuilder(String.valueOf(codeCh)); //跟据需要在尾部添加“=” if(srcStr.length()% 3 == 1) { code.append("=="); } else if(srcStr.length()% 3 == 2) { code.append("="); } //每76个字符加一个回车换行符(CRLF) int i= 76; while(i< code.length()) { code.insert(i, " "); i+= 76; } code.append(" "); return String.valueOf(code); } public static String decode(String srcStr) { //有效值检查 if(srcStr == null || srcStr.length() == 0) { return srcStr; } //检测密文中“=”的个数后将之删除,同时删除换行符 int eqCounter= 0; if(srcStr.endsWith("==")) { eqCounter= 2; } else if(srcStr.endsWith("=")) { eqCounter= 1; } srcStr= srcStr.replaceAll("=", ""); srcStr= srcStr.replaceAll(" ", ""); //跟据Base64编码表将密文(Encoding)转为对应Value,然后转为二进制位字串 char[] srcStrCh= srcStr.toCharArray(); StringBuilder indexBinStr= new StringBuilder(); String indexBin= null; for(int i= 0; i< srcStrCh.length; i++) { indexBin= Integer.toBinaryString(base64Code.indexOf((int)srcStrCh[i])); while(indexBin.length()< 6) { indexBin= "0"+ indexBin; } indexBinStr.append(indexBin); } //删除因编码而在尾部补位的“0”后得到明文的ASCII码的二进制位字串 if(eqCounter == 1) { indexBinStr.delete(indexBinStr.length()- 2, indexBinStr.length()); } else if(eqCounter == 2) { indexBinStr.delete(indexBinStr.length()- 4, indexBinStr.length()); } String asciiBinStr= String.valueOf(indexBinStr); //将上面得到的二进制位字串分隔成字节后还原成明文 String asciiBin= null; char[] ascii= new char[asciiBinStr.length()/ 8]; for(int i= 0; i< ascii.length; i++) { asciiBin= asciiBinStr.substring(0, 8); asciiBinStr= asciiBinStr.substring(8); ascii[i]= (char)Integer.parseInt(asciiBin, 2); } return String.valueOf(ascii); } public static void main(String[] args) { System.out.print(encode("I like your long long shadow.It just seems you are unhappy to say goodbye to me.")); System.out.print(" ---dokio--- "); System.out.print(decode("SSBsaWtlIHlvdXIgbG9uZyBsb25nIHNoYWRvdy5JdCBqdXN0IHNlZW1zIHlvdSBhcmUgdW5oYXBweSB0byBzYXkgZ29vZGJ5ZSB0byBtZS4=")); System.out.print(" ---dokio--- "); System.out.print(decode(encode("I like your long long shadow.It just seems you are unhappy to say goodbye to me."))); } }
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。
- 上一篇: js实现对象深度拷贝
- 下一篇: js实现深度拷贝