ISO 8583是发卡行和收单行之间实时通信的标准消息格式,广泛应用于所有主要的信用卡网络。无论您是在销售点设备上刷卡还是在线点击“购买”,最终很可能都会以ISO 8583消息的形式在商户的收单处理程序、卡网络和您银行的发卡处理程序之间传递。
早期,销售点设备或自动柜员机可能会直接构建并发送ISO 8583消息到收单行,但在当今的电子商务环境中,消息通常以更高级别的格式(例如JSON)从商户传递到支付处理程序,然后将其转换为卡网络基于ISO 8583的格式。这种方法通过将ISO 8583格式的复杂性从支付生态系统的其余部分抽象出来,简化了流程。
该标准最初于1987年定义,包括消息规范的总体结构以及核心字段的名称和长度,例如字段2中的卡号(“主账户号码”)和字段4中的交易金额。消息以4位的消息类型指示符代码开头,表示它是授权消息、冲正或其他某种消息类型。接下来是一个位图,告诉接收方哪些字段存在。它为网络保留了一些字段,以便包含特定于网络的信息,因此各种卡网络规范很快就通过一系列根本不重叠的嵌套字段出现了分歧。标准的后续版本大大增加了字段数量,减少了新实现对特定于网络行为的需求。例如,Visa的Base I规范有数千个客户端在其所有产品上运行,从大型机到自动柜员机等设备,这使得进行彻底的向后不兼容的更改变得几乎不可能。因此,这些规范在很大程度上仍然遵循1987年原始标准中规定的规则。
该标准还允许灵活地对每个字段进行序列化。例如,网络可以选择对所有字段使用EBCDIC(大型机青睐的8位编码方案),或选择使用打包BCD来尽可能节省数字字段的空间。这使得每个网络(如Visa、Mastercard和Discover)定义的规范最终演变成差异大于相似性的状态。
本文将首先介绍ISO 8583格式的基本结构,然后深入探讨其更复杂的嵌套子字段。最后,我们将探讨如何在代码中定义ISO 8583解析器,其基础是我们如何作为直接连接到VisaNet等网络的发卡处理程序处理交易。
**基本格式**
ISO 8583消息只能由具有共享规范的各方传输和接收,该规范详细说明了哪些字段存在以及存在于什么位置。ISO 8583消息类似于其他存储效率高的格式,并且与JSON等更冗长的格式不同,它只包含值,不包含字段名。基本消息包含描述发送的消息类型的“消息类型指示符”、解释哪些字段存在的位图以及字段本身。
**消息类型指示符**
消息类型指示符是一个四位数字代码,通知接收方正在发送的消息类型,例如授权消息或冲正。这告诉接收方期望消息中存在哪些字段以及不存在哪些字段。虽然规范定义了一组标准值(例如,授权请求为0100,授权响应为0110),但有些网络偏离了这些值,只保留了通用概念。指示符的序列化方式在不同网络之间也有所不同。某些网络使用打包BCD将其大小减少到2个字节,而其他网络则使用更简单的格式,例如ASCII或EBCDIC 4字节值。
**位图**
ISO 8583消息中的大多数字段都是可选的,要求发送方传达哪些字段存在以及哪些字段不存在。这是通过位图完成的,其中每个位如果字段存在则设置为1,如果不存在则设置为0。例如,第一个字节中的0110 1100表示字段2、3、5和6存在。第一个字节中的第一位用于传达是否包含第二个8字节位图,当存在超过64个字段时需要此位图。与消息类型指示符类似,位图本身的序列化方式在不同网络之间也有所不同:十六进制、二进制、ASCII或EBCDIC都是可能的选择。
**数据元素**
在位图之后,发送方按顺序序列化每个存在的字段。字段可以是原始的,包含单个值,例如字符串或整数,也可以是复杂的,在其内部包含嵌套字段。原始字段的序列化通常涉及以下因素的组合。
**编码**
即使对于其自由文本字段更喜欢ASCII的网络,序列化的ISO 8583消息通常也不是完全可读的纯文本,因为每个字段的编码根据字段类型而有所不同。通常有几种选择:
* EBCDIC或ASCII:通常用于自由格式文本,但有时也适用于所有字段,而不管内容如何。某些网络同时支持EBCDIC和ASCII,并根据参与者的偏好在这两种格式之间进行转换。主要网络通常默认为EBCDIC,因为这是IBM大型机在网络首次构建时选择的编码。
* 打包BCD:通常用于整数,每个数字占用4位。此编码与十六进制的0-9子集一致,使其对于数字数据来说非常节省空间。
* 二进制:偶尔用于固定长度整数,以低端或高端格式编码为1或2字节值,具体取决于系统的要求。
**可变或固定长度**
固定长度字段始终占用相同的字节数,并且需要填充以确保字段填充其整个长度。例如,金额字段通常为12位数字,右对齐,并用零填充——000000002412表示24.12美元。可变长度字段前面带有长度前缀,因此不需要填充,因为接收方首先解码长度指示符,然后使用它来确定要为字段本身提取多少个字节。
可变长度指示符的编码方式也取决于字段本身的内容:例如,数字收单机构 ID 可以编码为打包 BCD,其中 12 位数字编码为 6 个字节,但长度指示符仍将指示数字数量12,而不是字节数6。这为奇数长度整数创造了一种有趣的情况。以数字123为例:序列化后,它成为字节数组[1, 35],以二进制表示为00000001 00100011。当我们将其往返转换后,最终得到值为0123,因为我们无法区分第一个半字节中的0和我们用于123的填充零。因此,有必要包含长度指示符,它告诉我们的编码器实际的值长度应该为3位数字,以便我们可以修剪掉第一个填充零。
**嵌套消息**
虽然标准定义了核心字段,例如“交易金额”和“商户标识符”,但原始标准中预定义字段的列表最终成为卡网络功能开发的限制因素。为了解决这个问题,标准预留了一些“专用”字段,卡网络可以根据需要利用这些字段来序列化自定义数据。这正是规范在不同网络之间开始出现差异的地方,它们不仅选择了要传达的数据,而且还选择了每个嵌套字段的序列化方式。原始标准对该主题提供了有限的指导,这是一个后来版本试图解决的疏忽。
通常有三种主要方法可以序列化嵌套消息:
* **表:**每个字段通常具有固定长度,并且始终包含,无论是其实际值还是如果为空则替换为默认占位符值。
* **嵌套位图消息:**仅序列化存在的字段,使用顶级位图的简化固定长度版本来指示字段的存在。
* **标记长度值(TLV)消息:**每个字段都序列化为一个元组,其中包含字段编号(“标记”)、字段长度和字段值。此格式由单独的标准ISO 8825定义,该标准概述了ASN.1的编码规则。
每种嵌套消息类型的常见程度在不同网络之间有所不同。您可能会看到美国运通大量使用表,而只有Visa和中国银联使用嵌套位图消息。万事达卡主要坚持使用标记长度值消息,大多数卡网络都在慢慢向此方向发展,用于其所有新的子字段。
**表**
原始嵌套消息元素既是最简单的也是最复杂的。在其最简单的形式中,每个子字段都是按顺序序列化的,没有省略任何字段,始终产生固定数量的字节。这对于字段数量少的简单表效果很好,但对于具有大量可选字段的表会导致空间效率低下,因为必须为很少出现的字段发送不必要的填充字符。尝试调整格式以更好地支持此场景和其他场景引入了很大的复杂性,在这种情况下,实现者可能最好使用完全不同的子消息类型。
这种演变反映了软件开发中常见的模式,其中实现一开始很简单,但随着扩展的添加,逐渐变得越来越复杂。最终,您得到的解决方案对于客户端来说比全新的独立概念更复杂。
我们可以在原始字段之一字段43卡收单机构[0]名称或位置中看到基本表格式的显示:
**可变表**
每个表占用预定义数量的字节,并且始终存在,因为接收方希望按顺序解析每个字段。要省略上面的城市名称,我们将用13个空格填充它,从而导致相同的40字节总大小。如果经常省略城市名称,这种方法效率低下,迫使参与者传输空字符以指示数据不存在。相反,在表中有多个子字段是可选的情况下,实现可能允许发送方省略子字段,只要他们省略表中所有后续子字段即可。将以下示例中的子字段称为A、B和C,您可以发送以下组合:仅A、仅A和B,或A、B和C。这种类型的“伸缩”只有在表前面有可变长度指示符时才可能发生,该指示符向接收方传达他们应为该字段读取多少个字节,例如25个字节(A)、38个字节(A + B)或40个字节(A + B + C)。
此概念还用于在子消息中嵌套子消息,如下面的附加金额字段所示,我们可以在其中序列化多达6个每个20字节的附加金额:
当我们希望支持其中一个子字段占用可变数量的空间时,也会发生类似的扩展。回到我们的卡收单机构名称或位置表,如果我们想要支持最多200个字符的卡收单机构名称子字段,则原始方法将要求所有参与者用空格将值填充到最大长度200个字符。此方法虽然有效,但对于非常重视其节俭使用空间的标准来说效率低下。
为了解决这个问题,某些网络通过将可变长度子字段放在表的末尾来解决地址等字段的问题。这允许接收方简单地读取字段的其余部分,而无需填充,有效地消除了不必要的开销。但是,此方法每个表只允许一个可变长度子字段。
**半字节表**
某些表完全由使用打包BCD编码的数字字段组成。Visa的附加销售点信息字段就是这种情况,其中大多数字段仅包含一位枚举。如果我们像往常一样序列化这些字段,则每个单独字段最终都会得到一个完整的字节,并且每个字节的前4位会在零上浪费。很明显,这是一种效率极低的做法,规范规定每个一位字段应仅占用一个半字节,或半个字节。序列化后,它看起来像这样:
字节1:前4位表示终端类型,后4位表示终端输入功能
字节2:前4位表示芯片状态代码,后4位表示特殊状态指示器……等等。
**嵌套位图消息**
前面讨论的大多数复杂性都来自于尝试省略某些子字段,而无需浪费空间在空字符上以传达已省略了某个字段。嵌套位图消息通过在元素之前包含一个位图来解决此问题,从而向接收方传达哪些字段存在以及哪些字段已省略。这与顶级消息本身的序列化方式相同,区别在于嵌套子级消息位图通常更短且长度固定(例如,8个字节允许64位/字段)。
与表相比,这是一个改进,因为它完全消除了序列化空值的需要,从而节省了空间并简化了字段的解释。关于空值是否表示省略或承载实际值(例如,零金额)没有歧义。
它在保留向后兼容性方面也是一项改进。例如,您可以编写一个解析器,在其中忽略您尚未实现的任何新位图字段,从而允许网络添加新字段,直到位图的最大容量(例如,8个字节的64个字段)。但是,这通常不是网络的操作方式,因为添加新字段被视为重大更改,需要提前通知客户端。
**标记长度值消息**
ISO 8583标准的后续版本将完全独立的标准ISO 8825的部分内容整合到嵌套子消息中。这种方法非常强大,它完全消除了对其他嵌套消息类型(如表和位图消息)的需求。在此格式中,每个字段都序列化为一个元组,该元组包含标记、字段长度和字段值本身。与嵌套位图消息类似,这允许发送方省略任何缺少的字段,因为接收方可以依靠标记组件来识别正在解析的子字段。
与我们看过的其他两种消息格式不同,TLV格式是无序的。虽然标记在规范中按特定顺序定义,但发送方可以选择按任何方式序列化每个标记长度值元组,而不会导致接收方出现问题。这对于嵌套位图字段不起作用,在嵌套位图字段中,接收方读取位图以查看哪些字段(例如,字段1、3和5)存在,然后期望相应的数据按该顺序跟随。另一方面,标记长度值解析器将首先读取标记,并使用该标记来知道正在解析哪个字段。
标记长度值解析器实施起来稍微复杂一些,但为网络提供了最大的灵活性:预计参与者能够优雅地处理新的子字段。
**标记和长度格式**
标记和长度组件都以一种方式编码,其中接收方可以从初始字节的值确定每个组件使用的字节数。例如,如果需要后续字节,则标记仅设置第一个字节中的最后5位。
**层次结构**
通常,标记长度值消息结构为两层消息:顶级称为“数据集”,其中标记和长度组件使用的字节数是固定的,而标记长度值字段的较低级别依赖于标记和长度指示符的可变格式。这为添加更多字段提供了充足的空间。
**帧**
ISO 8583消息通常通过长期存在的TCP套接字发送,如“Visa:半个世纪的高可用性”中所述。因此,需要在消息周围有一层帧,以便接收方知道在TCP数据包流中一个ISO 8583消息在哪里结束以及另一个消息在哪里开始。这通常通过一个简单的长度指示符来完成,在ISO 8583消息前面加上一个4字节指示符,通知接收方应为ISO 8583消息本身读取多少个字节。
**特定于网络的标头**
一些卡网络(如Visa)还在帧消息长度标头和ISO 8583消息本身之间包含一个标头。这通常包含有关消息本身的元信息,例如消息是从哪里发送的以及要传递到哪里,以及网络是否因任何错误而拒绝了它。
**构建解析器**
解析基本ISO 8583消息非常简单,通常只需要实现一个位图解析器和每个字段的长度定义即可处理顶级处的原始元素。许多复杂性来自正确处理各种类型的嵌套子消息以及每个卡网络实现之间的细微差异。解决这种复杂性的一个有用技术是定义声明式地组合消息所需的核心构建块,而不是单独地实现每种不同的字段类型。
在Increase,我们使用Ruby并大量使用Sorbet类型系统。因此,我们使用Sorbet中的T::Struct类定义我们的ISO 8583解析器,这使我们在解析后获得了一个类型安全的message类。
`class Message < T::Struct`
`const :primary_account_number,`
` T.nilable(String),`
` extra:`
` Field.build(`
` message: Field::Message::Bitmap.new(index: 2),`
` encoding: Field::Encoding::BCD,`
` length:`
` Field::Length::Variable.new(`
` bytes: Field::Length::Variable::Bytes::ONE,`
` encoding: Field::Length::Variable::Encoding::BINARY,`
` ),`
` )`
`...`
这使得从卡网络提供的规范轻松映射到声明式解析器实现。定义合理的默认值很有用——固定长度的Integer类型默认情况下是右对齐并用零填充,但如果需要覆盖它,可以这样做:
`const :amount_settlement,`
` T.nilable(Integer),`
` extra:`
` Field.build(`
` message: Field::Message::Bitmap.new(index: 5),`
` encoding: Field::Encoding::BCD,`
` length: Field::Length::Fixed.new(size: 12),`
` padding:`
` Field::Padding.new(`
` character: Field::Padding::Character::SPACE,`
` justification: Field::Padding::Justification::LEFT,`
` ),`
` )`
类似地,我们使用其他T::Structs定义嵌套子消息,并在功能偏离网络到网络的地方留出配置空间:
`class VerificationAndTokenData < T::Struct`
` sig { override.returns(MessageTransform[VerificationAndTokenData]) }`
` def self.transform`
` TagLengthValue::MessageTransform.new(`
` struct_class: self,`
` tag_transform:`
` TagLengthValue::FixedTagTransform.new(`
` bytes: TagLengthValue::FixedTagTransform::Bytes::ONE,`
` ),`
` length_transform:`
` TagLengthValue::FixedLengthTransform.new(`
` bytes: TagLengthValue::FixedLengthTransform::Bytes::TWO,`
` ),`
` )`
` end`
` class VerificationData < T::Struct`
` sig { override.returns(MessageTransform[VerificationData]) }`
` def self.transform`
` TagLengthValue::MessageTransform.new(`
` struct_class: self,`
` tag_transform: TagLengthValue::VariableTagTransform.new,`
` length_transform: TagLengthValue::VariableLengthTransform.new,`
` )`
` end`
` const :postal_code,`
` T.nilable(String),`
` extra:`
` Field.build(`
` encoding: Field::Encoding::EBCDIC,`
` message: Field::Message::TagLengthValue.new(tag: 0xC0),`
` )`
` const :street_address,`
` T.nilable(String),`
` extra:`
` Field.build(`
` encoding: Field::Encoding::EBCDIC,`
` message: Field::Message::TagLengthValue.new(tag: 0xCF),`
` )`
` end`
` const :verification_data,`
` T.nilable(VerificationData),`
` extra:`
` Field.build(`
` message: Field::Message::TagLengthValue.new(tag: 0x66),`
# ...`
` const :missing_tags, T.nilable(T::Hash[Integer, String]), default: {}`
`end`
**错误处理**
上面标记长度值消息中的missing_tags字段存储了我们尚未实现的任何标记的哈希映射,并确保消息在不因新标记而崩溃的情况下仍然能够正确往返。这种面向未来的设计通常只需要用于标记长度值消息,但优雅地处理子消息中的本地错误也是一个有用的概念,可以应用于解析器的其余部分。虽然卡网络确实会验证消息的整体格式,但存在许多可能不会导致网络级别拒绝的细微错误。
这使得优雅的错误处理对于发卡处理程序尤其至关重要,发卡处理程序会接收来自全球大量收单行的授权请求。无法解析其中一条消息意味着您的持卡人会被拒绝,当您尝试亲自支付账单时,这是一种尤其令人沮丧的体验。但是,对于收单处理程序来说,这也是一个有用的概念,因为您经常会在您消息的响应中看到不符合标准的子字段。
为了优雅地处理嵌套子字段中的错误,您需要对正在解析的字节流进行分区,以便如果前面的字段发生错误,您可以继续处理下一个字段。以前面提到的附加销售点信息表为例:这些字段中的每一个都是枚举,您可能希望将其映射到编程语言中的枚举,而不是处理原始网络值(通常是整数)。如果您这样做,最终会看到一个您尚未处理的枚举,此时您的解析器应该能够将其记录为部分错误并继续下一个字段。
这里需要注意的一个重要细微差别是我们仍然需要确保我们将表序列化回合理的值(通常称为往返转换),尤其是在发送方期望在响应中镜像回此表的情况下。简单地完全省略您无法解析的值会导致表的总长度缩短,因此您需要用占位符(例如空格或零,具体取决于内容)替换未知值,或者保留您无法解析的原始值以将其放回消息中。
向上提升一个级别,您可以对整个子消息本身遵循类似的原则。如果我们已正确解析了标记长度值消息中的两个字段,但随后在尝试解释下一个标记的长度指示符时遇到错误,我们将需要丢弃子消息的其余部分并保留我们到目前为止解析的内容,即使这意味着可能丢弃在有故障的长度指示符之后跟随的子字段。
最后,在最顶层有一些我们无法恢复的错误。如果我们无法解析顶级字段之一的长度指示符,我们将无法解析消息的其余部分,并且它很可能是至关重要的,因此我们不应该尝试使用我们到目前为止获得的内容。这是您希望对解析器的每个可能出错的部分做出的决定——我们是否已经达到了无法返回的致命点,或者是否存在一种方法可以优雅地继续解析消息的其余部分,尽管我们刚刚遇到的错误?
**结论**
在本文中,我们探讨了在解析ISO 8583消息时出现的一些有趣的复杂性,并讨论了它们是如何从1987年定义的初始ISO 8583标准中产生的。当您使用Increase进行程序化卡处理时,我们会为您处理卡网络消息的解析,以便您可以专注于构建您的产品。我们这样做的方式避免隐藏或模糊细节,如我们的“无抽象:Increase API设计原则”博客文章中所述。如果这种方法与您产生共鸣,请联系我们的销售团队讨论将Increase用作您的发卡处理程序,或查看我们的API文档。
[0] 卡收单机构通常与商户同义:接受卡进行支付的实体。