概述
Base64是一种字符串编码格式,采用了A-Z,a-z,0-9,“+”和“/”这64个字符来编码原始字符(还有垫字符“=”)。一个字符本身是一个字节,也就是8位,而base64编码后的一个字符只能表示6位的信息。也就是原始字符串中的3字节的信息编码会变成4字节的信息。Base64的主要作用是满足MIME的传输需求。
在Java8中Base64编码已经成为Java类库的标准,且内置了Base64编码的编码器和解码器。
问题
偶然发现使用jdk8内置的Base64解码器进行解析的时候,会抛出java.lang.IllegalArgumentException: Illegal base64 character a
异常。
这非常奇怪,因为原文是使用jdk7里面的编码器进行编码的,理论上不至于发生这种不兼容的状况。
测试程序
还是来写程序测试一下问题到底在哪里。
测试程序使用了一个比较长的原文,主要是这个问题在原文较长的时候才会出现,如果原文较短(字节长度不超过57),那么不会有这个问题。
1 使用jdk7进行编码:
import sun.misc.BASE64Encoder;
public class TestBase64JDK7 {
private static final String TEST_STRING = "0123456789,0123456789,0123456789,0123456789,0123456789,0123456789,0123456789";
public static void main(String[] args) {
BASE64Encoder base64Encoder = new BASE64Encoder();
String base64Result = base64Encoder.encode(TEST_STRING.getBytes());
System.out.println(base64Result);
}
}
2 jdk7编码结果:
MDEyMzQ1Njc4Oe+8jDAxMjM0NTY3ODnvvIwwMTIzNDU2Nzg577yMMDEyMzQ1Njc4Oe+8jDAxMjM0
NTY3ODnvvIwwMTIzNDU2Nzg577yMMDEyMzQ1Njc4OQ==
3 使用jdk8对上面的编码结果进行解码:
import java.util.Base64;
public class TestBase64JDK8 {
public static void main(String[] args) {
String base64Result = "MDEyMzQ1Njc4Oe+8jDAxMjM0NTY3ODnvvIwwMTIzNDU2Nzg577yMMDEyMzQ1Njc4Oe+8jDAxMjM0\n" +
"NTY3ODnvvIwwMTIzNDU2Nzg577yMMDEyMzQ1Njc4OQ==";
Base64.getDecoder().decode(base64Result);
}
}
4 结果就如最开始描述的那样,会抛出异常:
Exception in thread "main" java.lang.IllegalArgumentException: Illegal base64 character a
at java.util.Base64$Decoder.decode0(Base64.java:714)
at java.util.Base64$Decoder.decode(Base64.java:526)
at java.util.Base64$Decoder.decode(Base64.java:549)
at com.francis.TestBase64JDK8.main(TestBase64JDK8.java:14)
难道说jdk7和jdk8在base64的处理上有什么不一样???
5 继续来看一下jdk8对原文的编码:
import java.util.Base64;
public class TestBase64JDK8 {
private static final String TEST_STRING = "0123456789,0123456789,0123456789,0123456789,0123456789,0123456789,0123456789";
public static void main(String[] args) {
String base64Result = Base64.getEncoder().encodeToString(TEST_STRING.getBytes());
System.out.println(base64Result);
}
}
6 jdk8编码结果:
MDEyMzQ1Njc4Oe+8jDAxMjM0NTY3ODnvvIwwMTIzNDU2Nzg577yMMDEyMzQ1Njc4Oe+8jDAxMjM0NTY3ODnvvIwwMTIzNDU2Nzg577yMMDEyMzQ1Njc4OQ==
至此针对比较长的原文进行base64编码可以得到如下结论:
- jdk7的编码结果包含换行;
- jdk8的编码结果不包含换行;
- jdk8无法解码包含换行的编码结果;
jdk8的编码结果使用jdk7进行解码,没有任何问题,不再演示。
现在问题原因基本清楚了,是由于jdk7的编码结果包含换行,导致jdk8解码的时候抛出异常。
但是为什么会有这种差异呢?难道使用的base64的标准还不一样?
问题排查
继续排查问题,先从类注释入手,看看是不是理解有误。
1 先来看看jdk8中的Base64类注释,这里只列出一些关键内容:
/**
* This class consists exclusively of static methods for obtaining
* encoders and decoders for the Base64 encoding scheme. The
* implementation of this class supports the following types of Base64
* as specified in
* <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a> and
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
*
* <ul>
* <li><a name="basic"><b>Basic</b></a>
* <p> Uses "The Base64 Alphabet" as specified in Table 1 of
* RFC 4648 and RFC 2045 for encoding and decoding operation.
* The encoder does not add any line feed (line separator)
* character. The decoder rejects data that contains characters
* outside the base64 alphabet.</p></li>
...
* @author Xueming Shen
* @since 1.8
*/
大意是说:
这个类包含了base64编码格式的编码方法和解码方法,而且实现是按照rfc4648和rfc2045两个协议来实现的。
编码和解码操作是照着两个协议中的'Table 1'中指定的'The Base64 Alphabet'来的。编码器不会添加任何换行符,解码器只会处理'The Base64 Alphabet'范围内的数据,如果不在这个范围内,解码器会拒绝处理。
看到这里就可以理解为什么jdk8的编码结果不包含换行了。
另外,基本上可以猜到为什么jdk8无法解码jdk7的编码结果了(换行符应该不在
The base64 alphabet
当中)。
2 先来看一眼两个标准中的the base64 alphabet
(两个标准中的这个表是一样的):
Table 1: The Base 64 Alphabet
Value Encoding Value Encoding Value Encoding Value Encoding
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 (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y
并不包含换行符,这就可以解释为什么jdk8无法解码包含换行的编码结果。
3 再来看一下jdk7中sun.misc.BASE64Encoder
的类注释:
This class implements a BASE64 Character encoder as specified in RFC1521.
This RFC is part of the MIME specification as published by the Internet Engineering Task Force (IETF).
Unlike some other encoding schemes there is nothing in this encoding that indicates where a buffer starts or ends.
This means that the encoded text will simply start with the first line of encoded text and end with the last line of encoded text.
这个实现是按照RFC1521来的,类注释中并没有关于编码或者解码约束的说明。
4 那继续看一下rfc1521的关键部分(链接:https://tools.ietf.org/html/rfc1521)。
在5.2. Base64 Content-Transfer-Encoding
章节有如下内容:
The output stream (encoded bytes) must be represented in lines of no
more than 76 characters each. All line breaks or other characters
not found in Table 1 must be ignored by decoding software. In base64
data, characters other than those in Table 1, line breaks, and other
white space probably indicate a transmission error, about which a
warning message or even a message rejection might be appropriate
under some circumstances.
这里明确规定了:
- 编码结果的每一行不能超过76个字符;
- 解码的字符必须在:
Tbale 1
(也就是之前提到的the base64 alphabet
)、换行符和空白符这个范围内;
这就是为什么jdk7的编码结果包含换行。
这样根据类注释和rfc协议内容,就可以解释上面通过测试代码得到的结论,也就可以理解为什么会产生这个问题。
‘sun’开头的包并不属于java规范,是sun公司的实现,所以jdk7中的这种base64编码方式并不是java的规范。
解决办法
那么,怎么解决这个问题呢:
1. 使用apache common包中的org.apache.commons.codec.binary.Base64
类进行编码和解码;
2. 编码之后或解码之前去除换行符;
3. 编码和解码使用相同的jdk版本;
其他Base64库
看看其他类库是怎么处理base64的。
1. Apache Common
Apache Common中的org.apache.commons.codec.binary.Base64
类是基于rfc2045实现的,根据类注释可以了解到此实现解码时忽略了所有不在the base64 alphabet
范围内的字符,所以该实现可以处理包含换行符的base64编码结果。
同时该类的编码方法提供了参数,用于指定编码结果长度在超过76个字符的时候是否添加换行,默认不换行。
- Spring Core
Spring Core提供了Base64Utils类,该类只是一个工具类,并没有实现任何协议。
- 优先使用java8中的
java.util.Base64
类进行编码和解码; - 如果
java.util.Base64
不存在,则会使用org.apache.commons.codec.binary.Base64
; - 如果都不存在,则会报错
协议简述
通过上面的排查步骤可以看到,rfc1521
、rfc2045
和rfc4648
中关于base64的部分似乎不太一样,接下来分别简单看一下这三个协议是如何规范base64编码的换行的。
-
rfc1521(链接:https://tools.ietf.org/html/rfc1521)
该协议是关于MIME的,Base64是MIME支持的一种编码类型。关键内容5.2. Base64 Content-Transfer-Encoding
章节已经在上文中简单阐述过了,主要是规定了:编码结果每行长度和解码字符的范围。
该协议已经被淘汰。
jdk7基于该协议实现base64,所以编码结果会包含换行符。MIME:Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型。是一个互联网标准,最早用于电子邮件系统,后来被应用到浏览器。服务器会将它们发送的多媒体数据的类型告诉浏览器,而通知手段就是说明该多媒体数据的MIME类型。
-
rfc2045(链接:https://tools.ietf.org/html/rfc2045)
该协议同样是关于MIME的,是rfc1521的更新版本,关键内容
6.8. Base64 Content-Transfer-Encoding
章节,其中关于编码结果长度和解码字符范围的规定与rfc1521并没有什么差别。 -
rfc4648
该协议是关于base16、base32和base64编码的。关于编码结果每行长度的说明在
3.1. Line Feeds in Encoded Data
章节:
MIME is often used as a reference for base 64 encoding. However,
MIME does not define "base 64" per se, but rather a "base 64 Content-
Transfer-Encoding" for use within MIME. As such, MIME enforces a
limit on line length of base 64-encoded data to 76 characters. MIME
inherits the encoding from Privacy Enhanced Mail (PEM) [3], stating
that it is "virtually identical"; however, PEM uses a line length of
64 characters. The MIME and PEM limits are both due to limits within
SMTP.
Implementations MUST NOT add line feeds to base-encoded data unless
the specification referring to this document explicitly directs base
encoders to add line feeds after a specific number of characters.
大意是:
MIME协议通常作为base64协议的引用。但是MIME协议并没有定义'base64',而是定义了'base64 内容传输编码'。因此MIME将base64编码的数据的长度限制为76个字符。
...
MIME和PEM关于长度的限制都是用于SMTP的。
该协议的实现在编码结果中不能添加换行符,除非引用了该文档的实现中,明确说明在特定长度之后添加换行符。
jdk8的Base64类是基于rfc2045和rfc4648实现的,根据上文列出的协议内容可以确定,该类的编码结果不会包含换行符,而且在类的注释中明确说明了不会添加换行符。