对接银行支付,自己的demo可以调通,放到项目里,却总提示验签失败。原来竟是因为...
原因是 字符集(charset)不一致
对接一个银行支付通道的支付API,自己java写的demo可以调通,放到项目工程里,部署到环境上,总是收到验签失败的响应。这个问题,困扰我们的开发大兄弟长达一个星期。
对接通道接口联调不通,常见的场景有许多,如:
- 签名原串需要对key进行排序。不同的排序算法会导致联调不通。
- json序列化,不同json序列化对数字的支持不同。可能会导致联调不通。
- 参数大小写拼写错误,会导致联调不通。
- 等等。
而这次呢,却是字符编码导致的。
各个加密算法,都是基于字节数据进行加密的。例如,下面的md5加密工具方法,在使用MD5算法加密时,首先要把程序中的字符串转换为byte[],见下方代码中的 text.getBytes()。
public static String md5(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(text.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b & 0xff)); } return sb.toString(); }
上面代码调用 String#getBytes将字符串转换为byte数组。而在这个String与byte[]的转换中,涉及到一个很重要的东西————字符编码。对于相同的字符串,不同的编码格式,得到的结果可能不同,对于中文汉字来说正是如此。
其实,技术点就可以简化为如何来理解 String#getBytes()、String#getBytes(charsetName)。与之对应的是 String的构造器String(byte[])、String(byte[], charsetName) 。这就是 String 与 byte数组 的数据互转。我们看一下它们的源码:
// - - - - - 构造器的重载 - - - - - /** * Constructs a new String by decoding the specified array of bytes using the platform's default charset. The length of the new {@code String} is a function of the charset, and hence may not be equal to the length of the byte array. * * @since JDK1.1 */ public String( byte bytes[]) { this (bytes, 0 , bytes.length); } /** * Constructs a new {@code String} by decoding the specified array of bytes using the specified charset. The length of the new {@code String} is a function of the charset, and hence may not be equal to the length of the byte array. * * @param bytes - The bytes to be decoded into characters * @param charsetName - The name of a supported {@linkplain java.nio.charset.Charset charset} * @throws UnsupportedEncodingException - If the named charset is not supported * * @since JDK1.1 */ public String( byte bytes[], String charsetName) throws UnsupportedEncodingException { this (bytes, 0 , bytes.length, charsetName); } // - - - - - getBytes方法的重载 - - - - - /** * Encodes this String into a sequence of bytes using the platform's default charset, storing the result into a new byte array. * * @since JDK1.1 */ public byte [] getBytes() { return StringCoding.encode(value, 0 , value.length); } /** * Encodes this {@code String} into a sequence of bytes using the named charset, storing the result into a new byte array. * (使用指定的字符集将此字符串编码为字节序列,并将结果存储到一个新的字节数组中。) * * @param charsetName - The name of a supported {@linkplain java.nio.charset.Charset charset} * @return The resultant byte array * @throws UnsupportedEncodingException - If the named charset is not supported * * @since JDK1.1 */ public byte [] getBytes(String charsetName) throws UnsupportedEncodingException { if (charsetName == null ) throw new NullPointerException(); return StringCoding.encode(charsetName, value, 0 , value.length); } |
其中,在两个没有charset参数的String(byte bytes[])、getBytes()方法里,均会获取JVM默认字符集。String csn = Charset.defaultCharset().name();。我们来看一下java.nio.charset.Charset#defaultCharset源码:
/** * Returns the default charset of this Java virtual machine. * The default charset is determined during virtual-machine startup and typically depends upon the locale and charset of the underlying operating system. * * @return A charset object for the default charset * * @since 1.5 */ public static Charset defaultCharset() { if (defaultCharset == null ) { synchronized (Charset. class ) { String csn = AccessController.doPrivileged( new GetPropertyAction( "file.encoding" )); Charset cs = lookup(csn); if (cs != null ) defaultCharset = cs; else defaultCharset = forName( "UTF-8" ); } } return defaultCharset; } |
字符集的选择在字符串和字节数组之间的转换中非常重要,特别是当涉及到非ASCII字符时。确保在转换过程中使用一致的字符集,才能正确地保留和还原字符串的内容。我们通过下面的代码来直观地感受一下区别。当 text 里包含 汉字时,不同的字符集在编码时使用不同的编码方式和字节数,编码后的结果就会有所不同; 当我们修改一下 `text = "I like 3 things in this world.";` 时,由于文本中只包含 ASCII 字符,UTF-8、GB2312 和 ISO-8859-1 都使用相同的编码来表示 ASCII 字符,因此最终的字节长度都是相同的。这就是上面String构造器javadoc里的“The length of the new {@code String} is a function of the charset, and hence may not be equal to the length of the byte array.”这句话的含义。
String text = "我是中国人" ; System.out.println(text1.getBytes( "ASCII" ).length); // 返回:5 System.out.println(text.getBytes( "UTF-8" ).length); //返回:15 System.out.println(text.getBytes( "GB2312" ).length); //返回:10 System.out.println(text.getBytes( "ISO-8859-1" ).length); //返回:5 |
由上面java.nio.charset.Charset#defaultCharset源码可以看到,Java的默认字符编码通常是平台的默认编码,这个取决于操作系统。例如,在中文Windows系统上,它可能是GBK或GB2312。
Tomcat默认的字符编码是ISO-8859-1。
我们这位开发大兄弟在对接银行通道时,使用java编写的demo,用到的字符集是 GB2312, 而项目是部署到tomcat容器里的, 两者字符集不同。 所以,出现一个行而另一个不行就不难理解了。
Base64区分字符集吗?
Base64不区分字符集。 下面两点,可以帮助你理解。
Base64 encode 和 decode 都是基于byte[]进行编码,返回的也是byte[]。
再一点,Base64 编码表使用固定的字符集,包括大小写字母、数字和两个额外的字符作为填充。 我们常见的字符集有 ASCII、Unicode、UTF-8、ISO-8859-1、GBK、GB2312等,其中ASCII是最早的字符集,它定义了 128 个包括字母、数字和一些特殊字符的编码。其他那些字符集均兼容ASCII(重点)。
下图进一步帮助你来直观地理解。
由图中可以看到, base64本身的编码和解码都是针对ASCII编码的byte[]数据进行操作,因此,不涉及字符集。 可能存在问题的,则是我们程序的原始字符串 text。 text.getByte(charset) 与 最后的 new String(byte[], charset) ,当其中两个的 charset 不一致时, 结果就会不一致。 以下面代码为例,执行后可以发现 afterText 与 text 的内容不同了。 归根结底, 这里的技术点依然是上面的 字节数据 与 字符串 的转换。
String text = "我是中国人i love China" ; String afterText = new String(text.getBytes(StandardCharsets.ISO_8859_1), "UTf-8" ); |
关于base64解码,rt.jar 里的 java.util.Base64.Decoder类里,有如下两个重载方法。其中第一个重载里, 默认使用了 ISO-8859-1 对字符串进行编码。
public byte [] decode(String src) { return decode(src.getBytes(StandardCharsets.ISO_8859_1)); } public byte [] decode( byte [] src) { ... } |
字符 / 字节 / 字符集 ,傻傻分不清?
字符和字节是表示数据的不同表现形式。
字符(Character):字符是指文本中的单个字符,例如字母、数字、标点符号、汉字、特殊符号(如拉丁字母)等。在计算机中,字符通常使用Unicode字符集进行表示。在Java中,表示一个字符使用`char`类型。表示一个字符序列,可以使用String、StringBuilder,它们实现了相同的接口 CharSequence。
字节(Byte):字节是计算机中存储数据、传输的最小单位。一个字节由8个二进制位组成,可以表示从0到255之间的整数。在计算机中,所有数据需要以字节的形式进行存储和传输。
上面提到的,我们编码中使用的是文本字符(character)数据,而数据传输和存储使用字节(byte)的形式。那么,就需要在这两种数据形式之间做数据转换,即字符数据的编码和解码(codec),codec中就涉及到了字符集(charset)。
字符集(Character Set):字符集是一套字符的集合,每个字符在字符集中都有一个唯一的编码值。字符集定义了字符与字节之间的映射关系。常见的字符集包括ASCII、UTF-8、UTF-16、ISO-8859-1等。
在字符串编码和解码过程中,字符集起到了关键的作用。正确选择和匹配字符集是确保字符能够正确存储和传输的关键。
编码(Encoding):编码是将字符转换为字节序列的过程。编码方案根据字符集的定义来确定如何将字符映射为字节。
解码(Decoding):解码是将字节序列转换回字符的过程。解码方案根据字符集的定义来确定如何将字节映射回字符。
字符集小常识
字符集(charset)是一种规定了字符与二进制数据之间对应关系的编码方案。它定义了如何将字符映射到二进制表示形式,以便计算机能够存储、处理和传输文本数据。
字符集中的字符可以是字母、数字、符号以及其他特定语言或地区的特殊字符。常见的字符集包括 ASCII、Unicode、UTF-8、ISO-8859-1 等。
ASCII(American Standard Code for Information Interchange)是最早的字符集,定义了 128 个字符的编码,包括基本的拉丁字母、数字和一些特殊字符。然而,ASCII 只适用于英语和一些西欧语言。
为了满足全球范围内的多语言需求,Unicode 被引入,它定义了几乎所有语言的字符集。Unicode 使用唯一的编码值来表示每个字符,其编码空间非常大。
UTF-8(Unicode Transformation Format-8)是一种对 Unicode 进行编码的方式,它使用变长编码来表示字符。UTF-8 是目前最常用的字符集编码方式之一,它兼容 ASCII,并支持各种语言的字符表示。
ISO-8859-1(Latin-1)是一种单字节字符集,它是 ASCII 的扩展,包含了西欧语言的字符。它是许多早期计算机系统默认的字符集编码。
GBK(GuoBiao KangXi)和 GB2312(GuoBiao 2312) 是中国国家标准的字符集,用于表示中文字符。它们都是在 ASCII 的基础上进行扩展的,保留了 ASCII 字符的编码,并添加了更多的中文字符。
字符集的选择取决于所处理数据的特定需求。在进行文本处理、编码转换、网络通信等操作时,正确地理解和使用字符集非常重要,以确保数据的正确性和互操作性。
ref:
本文物料素材
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/18164408