JDK1.8中java.util.Base64 编码解码

由于最近的开发中,在进行加解密的时候,抛异常Illegal base64 character d,顾这里记录一下问题所在及处理方式

Posted by Steven on 2022-12-06
Estimated Reading Time 6 Minutes
Words 1.4k In Total
Viewed Times

一、问题发现

工作中遇到的加签、验签的时候,我们有如下几个方法:

验证签名:

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
26
27
28
29
30
31
32
33
34
35
/**
* 签名验证 JAVA示例
*
* @param httpBody
* @param publicKey
* @param signStr
* @param timestamp
* @param nonce
* @return
* @throws Exception
*/
public static boolean verify(String httpBody, String publicKey, String signStr, Long timestamp, String nonce)
throws Exception {
StringBuffer buffer = new StringBuffer();
buffer.append(timestamp).append("\n");
buffer.append(nonce).append("\n");
buffer.append(httpBody).append("\n");
String message = buffer.toString();
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(string2PublicKey(publicKey));
sign.update(message.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(signStr.getBytes(StandardCharsets.UTF_8)));
}

/**
* string2PrivateKey
*
* @param publicKey
* @return
* @throws Exception
*/
public static PublicKey string2PublicKey(String publicKey) throws Exception {
byte[] decoded = Base64.getDecoder().decode(publicKey);
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
}

生成签名:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 签名生成
*
* @param privateKeyStr
* @param timestamp
* @param method
* @param url
* @param body
* @return
* @throws Exception
*/
public static String getSignature(String privateKeyStr, Long timestamp, String nonce, String method, String url,
String body)
throws Exception {
StringBuffer buffer = new StringBuffer();
buffer.append(method).append("\n");
buffer.append(url).append("\n");
buffer.append(timestamp).append("\n");
buffer.append(nonce).append("\n");
buffer.append(body).append("\n");
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(string2PrivateKey(privateKeyStr));
sign.update(buffer.toString().getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(sign.sign());
}


/**
* string2PrivateKey
*
* @param privateKeyStr
* @return
*/
public static PrivateKey string2PrivateKey(String privateKeyStr) {
PrivateKey prvKey = null;
try {
byte[] privateBytes = Base64.getDecoder().decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
prvKey = keyFactory.generatePrivate(keySpec);
} catch (Exception ex) {
ex.printStackTrace();
}
return prvKey;
}

其中我们发现,以上在进行签名生产或者验证签名的时候,都会用到Base64,这里不深究具体的算法,只说Base64,而我在JDK1.8的环境下,不论是生产签名还是验证签名,在我们的业务场景下,都会报错:

1
java.lang.IllegalArgumentException: Illegal base64 character 

二、问题排查

查询相关文档:

Base64 是一种常见的字符编码解码方式,一般用于将二进制数据编码为更具可读性的 Base64 进制格式。

在 Java 6 ( JDK 1.6 ) 之前, JDK 一直没有包含 Base64 的实现类。因此大部分人都使用 Sum/Orale JDK 里面的 sun.misc.BASE64Encode 和sun.misc.BASE64Decode。然后这也成为很多 Java 开发者的习惯。一直沿用到今天的 Java8 中还有人在用。

JDK 1.6 虽然添加了 Base64 的实现。但是,非常隐秘,竟然是在 javax.xml.bind 包下的 DatastypeConvert 类中的两个静态方法 parseBase64Binary 和 printBase64Binary.

Java 8 终于把 Base64 扶正了,在 java.util 包下提供了 Base64 类用于编码和解码 Base64 数据。

Java 8 中的 java.util.Base64 类提供了三种类型的 Base64 编码解码格式:

  1. 简单类型( simple ) : 编码字符只包含 A-Za-z0-9+/ 等 64 个字符。且编码的时候不会包含任何换行符 ( \r 、 \n 、\r\n )。解码的时候也只会解码 A-Za-z0-9+/ 内的字符,超出的则会被拒绝。

  2. URL : 编码字符只包含 A-Za-z0-9+_ 等 64 个字符。和 简单 相比,就是把 / 换成了 _ 。因为没有 / 字符,因此这种编码方式非常适合 URL 和文件名等。

  3. MIME : 编码会被映射为 MIME 友好格式:每一行输出不超过 76 个字符,而且每行以 \r\n 符结束。但末尾行并不会包含 \r\n

内部类DecoderEncoder

java.util.Base64 还包含了两个内部静态类,分别实现了 RFC 4648 和 RFC 2045 中规范的 Base64 编码和解码方式。

内部类 说明
static class Base64.Decoder 该类实现使用 RFC 4648 和 RFC 2045 中规定的 Base64 解码方案解码数据
static class Base64.Encoder 该类实现使用 RFC 4648 和 RFC 2045 中规定的 Base64 编码方案编码数据

静态方法

java.util.Base64 类提供的都是静态方法。下表列出了这些静态方法

方法 说明
Base64.Decoder getDecoder() 返回一个 Base64.Decoder 类型的 简单 解码器
Base64.Encoder getEncoder() 返回一个 Base64.Encoder 类型的 简单 编码器
Base64.Decoder getMimeDecoder() 返回一个 Base64.Decoder 类型的 MIME 解码器
Base64.Encoder getMimeEncoder() 返回一个 Base64.Encoder 类型的 MINE 编码器
Base64.Encoder getMimeEncoder(
int lineLength, byte[] lineSeparator)
返回一个 Base64.Encoder 类型的使用特定长度和行分隔符的 MINE 编码器
Base64.Decoder getUrlDecoder() 返回一个 Base64.Decoder 类型的 URL 和文件名安全的解码器
Base64.Encoder getUrlEncoder() 返回一个 Base64.Encoder 类型的 URL 和文件名安全的编码器

而我们使用Base64.getDecoder().decode()这种方式的话,加解密的时候是不能包括 \r 、 \n 、\r\n等字符的

三、解决问题

我们发现其实Base64提供了一种友好的方法,即MIME,该方式输出隐射到MIME友好格式。输出每行不超过76字符,并且使用’\r’并跟随’\n’作为分割。编码输出最后没有行分割。

因此最初始的几个方法中的getDecoder()或者getEncoder()替换getMimeDecoder()getMimeEncoder(),发现问题解决

举个例子,拿验证签名来说:

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
26
27
28
29
30
31
32
33
34
35
36
/**
* 签名验证 JAVA示例
*
* @param httpBody
* @param publicKey
* @param signStr
* @param timestamp
* @param nonce
* @return
* @throws Exception
*/
public static boolean verify(String httpBody, String publicKey, String signStr, Long timestamp, String nonce)
throws Exception {
StringBuffer buffer = new StringBuffer();
buffer.append(timestamp).append("\n");
buffer.append(nonce).append("\n");
buffer.append(httpBody).append("\n");
String message = buffer.toString();
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(string2PublicKey(publicKey));
sign.update(message.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getMimeDecoder().decode(signStr.getBytes(StandardCharsets.UTF_8)));
}

/**
* string2PrivateKey
*
* @param publicKey
* @return
* @throws Exception
*/
public static PublicKey string2PublicKey(String publicKey) throws Exception {
byte[] decoded = Base64.getMimeDecoder().decode(publicKey);
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
}

当然这只是解决了我当下这个问题,就是有一些字符的原因导致的问题,也有可能是其他的问题,这里并不是说该方法是所有类似问题的解决方案,谨慎判断和参考


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !