一、问题发现
工作中遇到的加签、验签的时候,我们有如下几个方法:
验证签名:
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
|
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))); }
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
|
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()); }
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 编码解码格式:
-
简单类型( simple ) : 编码字符只包含 A-Za-z0-9+/
等 64 个字符。且编码的时候不会包含任何换行符 ( \r
、 \n
、\r\n
)。解码的时候也只会解码 A-Za-z0-9+/
内的字符,超出的则会被拒绝。
-
URL : 编码字符只包含 A-Za-z0-9+_
等 64 个字符。和 简单 相比,就是把 /
换成了 _
。因为没有 /
字符,因此这种编码方式非常适合 URL 和文件名等。
-
MIME : 编码会被映射为 MIME 友好格式:每一行输出不超过 76 个字符,而且每行以 \r\n
符结束。但末尾行并不会包含 \r\n
。
java.util.Base64
还包含了两个内部静态类,分别实现了 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
|
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))); }
public static PublicKey string2PublicKey(String publicKey) throws Exception { byte[] decoded = Base64.getMimeDecoder().decode(publicKey); return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); }
|
当然这只是解决了我当下这个问题,就是有一些字符的原因导致的问题,也有可能是其他的问题,这里并不是说该方法是所有类似问题的解决方案,谨慎判断和参考
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !