一串卡号里,只有最后一位是被算出来的。开头的几位由卡组织规定,中间的账户数字由发卡行分配,而收尾那一位必须让整串数字通过一道固定的加法检验——这道检验就是 Luhn 算法,也叫模 10 校验。它的规则短到能抄在便利贴上,却决定了你的表单会接受哪些输入、拦下哪些输入。
花几分钟把它看懂有一个很实在的好处:当别人告诉你某个号码校验通过时,你能立刻判断这句话的分量有多轻。答案是,很轻。
Luhn 算法为什么存在
Luhn 算法属于校验和算法:它不解读数字的含义,只看一组数字加总之后能不能被 10 整除。做法是把数字按位置分成两组,一组原样保留,另一组先乘以 2 再累加,最后比对总和。如果总和是 10 的整数倍,这串数字就被认为没有抄错。
它诞生的年代还没有联网的实时查询。那时候卡片信息靠人工抄写、靠纸带传递,一位数字打错就可能让整笔交易对不上账。用一位冗余数字换掉大部分手误,是当时性价比最高的选择。这套方法由 IBM 的 Hans Peter Luhn 在 1950 年代提出,后来经由 ISO/IEC 7812 成为银行卡号的通行检验方式。
卡号末尾那一位,正是为了让总和刚好整除而反推出来的。发卡行排好账户数字之后,逐位试算出唯一一个能填在末位的数字。也就是说,校验位本身携带的信息量为零——它不描述账户,只是前面所有数字的一个浓缩副本。
完整规则就只有四条
把算法拆开,规则少得有点出乎意料。从最右边开始向左数,处于偶数位上的数字先乘以 2;乘完结果超过 9 的,减去 9,这一步等价于把两位数的两个数字相加;把所有处理过的数字与没处理的数字相加;总和是 10 的整数倍才算通过。
有一点常被写错:最右边那一位校验位自己不参与加倍,但它照样要计入总和。正因为它的位置固定,才能反推出它的值。
这四条规则可以写在任何一张便签上,公开在医院、学校、物流系统里都一样。使用固定权重的代价是它能表达的错误类型比较有限;只使用一个校验位,也是同一种取舍的结果。记住这一点,后面的结论就顺理成章了。
手算一遍:从右往左加倍
拿公开文档里最常被引用的那张通用测试卡来演示。它有 16 位,数字是 4242 重复四遍。从右往左数,第 2、4、6 直到第 16 位需要乘 2。
这张卡的结构恰好让手算很省事:所有偶数位上的数字都是 4,乘 2 之后都等于 8,八个数相加得 64;剩下的八位全是 2,原样相加得 16。64 加 16 等于 80,80 能被 10 整除,所以它通过校验。
换个方向检查也成立:随便改动其中一位数字,总和就会偏离 10 的倍数,检验当场失败。这解释了为什么手工抄错一位往往会被表单立刻拦下——在大多数情况下,少一位、多一位、错一位都会改变总和的余数。
除了卡号,还有谁在用它
卡号之外,同一套思路还出现在别的编号系统里,也因此衍生出几个近亲。
这套校验并不专属于支付行业。国际移动设备识别码用同一套思路检查设备串号,一些国家的身份号码与社保号码也采用模 10 校验,物流与票务系统在编号设计里用类似手法减少抄写错误。它几乎是所有编号系统在不想引入重校验位时最先想到的方案。
也存在同一思路的变体。例如图书编号使用的模 11 加权求和,因为模数与权重不同,它能多挡住几类错误,代价是实现更啰嗦,而且会产生一个不是数字的尾码。理解它的价值并不在记住这一个公式,而在认识校验和这个工具族:它们都只做一件事,用少量冗余数字换取输入错误被当场发现的概率。
对照着看会更清楚:校验和面向的是手误,密码学面向的是对手。把这两件事混在一起,是很多卡号校验代码出问题的根源。校验和的设计目标从来不是让错误无法构造,而是让错误不容易被无意间写出来。
把它和近亲放在一起看,边界会更清楚:
| 算法族 | 主要阻挡的失误 | 能否在没有密钥的情况下算出合法值 |
|---|---|---|
| 银行卡号用的模 10 校验 | 单错位、大部分相邻换位 | 能,规则完全公开 |
| 图书编号用的模 11 校验 | 单错位、全部相邻换位 | 能,规则同样公开 |
| 消息摘要与数字签名 | 篡改与伪造 | 不能,需要持有密钥 |
这张表想说明的只有一件事:左边两行是纠错工具,右边一行是安全工具,它们的工作方式完全不同。
换位错误它抓得住吗?
答案是抓住大部分,但不是全部。相邻两位数字互换时,多数情况会因为加倍位置发生改变而被察觉,因为两个数字的权重不同。但有一类例外:0 与 9 互换时两种排列算出的结果完全相同——0 乘 2 得 0,9 乘 2 超过 9 减 9 得 9,最终总和一模一样。
所以准确的说法是:Luhn 算法能发现单个数字打错和大部分相邻换位错误。凡是把这个算法描述成「能发现所有录入错误」的说法,都是不准确的。
通过校验位等于这张卡是真的吗?
不等于,而且差得很远。校验位只能回答一个问题:这串数字有没有被抄错。它回答不了账户是否存在、这张卡是否还有效、额度够不够、输入的人是不是持卡人。
原因很直接:它是一道公开、可逆的算术题。知道规则的人可以在几毫秒内为任意前缀补出一个合法末位,一个脚本每秒能造出成千上万个校验通过的号码。校验位的全部意义是挡住手误,而对抗攻击需要的是完全另一类工具。
因此,一串仅通过模 10 检验的号码,只是一串形状正确的数字。本站生成的测试号码都属于这一类:结构上合法,但从未发行给任何人,也不属于任何账户。
给开发者:校验顺序比算法本身更容易出错
实现 Luhn 算法本身并不难,难的是它在整条校验链里的位置。建议把顺序固定下来:先去掉空格与连字符,再确认剩下的字符全部是数字,然后检查长度是否落在合理范围,接着识别首位数字对应的卡组织,最后才跑模 10 检验。
把清洗与校验拆成两个独立的步骤,调用处就不容易出现「传进来一个带空格的字符串」这种问题。另外,遍历时的方向最容易写错:用一个标记量表示这一位要不要加倍,从最右端开始,每处理一位翻转一次,这样既不必计算长度的奇偶,也不会在奇位数号码上翻车。
还有一个高频错误是忘记处理加倍后大于 9 的情况,直接拿两位数字参与求和。短期看它只是让部分号码失败,长期看会让你的实现与其他系统不一致,出现本地通过、网关拒绝这类难以定位的现象。
最后提醒一句:模 10 检验只能当输入纠错,绝不能当安全边界。它是错字检测器,不是防线。校验顺序的完整讨论见在代码里校验信用卡号。
下一步
想看清号码各段的含义,接着读信用卡号格式;想知道这个校验在真实流程里排在什么位置,读在代码里校验信用卡号;需要一批现成的号码做联调,直接用信用卡号生成器按卡组织批量生成。
如果刚开始接触这个主题,建议先回到测试信用卡号是什么,把校验位不等于真卡这一点记牢,再回来研究算法细节。顺序反过来的话,很容易把一道算术题误当成一道安全防线。