对接银行支付,自己的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 == nullthrow 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:
本文物料素材

Base64编码详解

热门相关:我能看到隐藏机缘   甜蜜婚令:陆少的医神娇妻   万界点名册   嫁入豪门后我养崽盘大佬   傲天弃少