多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

银行卡号验证与Luhn算法的Java实现

银行卡号验证与Luhn算法的Java实现 银行卡号验证与Luhn算法的Java实现引言绑卡是电商、支付、理财类应用的标配功能。用户输入 16 位银行卡号时如果手一抖打错一位系统是等到银行接口报卡号不存在才返回错误还是能在前端/本地就拦截下来业界标准答案是 Luhn 算法也叫模 10 算法它在不查任何数据库的前提下仅凭卡号本身就能识别绝大多数录入错误。1954 年IBM 工程师 Hans Peter Luhn 申请了这项专利六十年后的今天全球几乎所有银行卡号、部分手机 IMEI、ISBN-13 书号、甚至一些优惠券码都在用它。本文不讲背一段代码式的速成而是从算法规则出发推导它的数学本质再给出可以直接落地的 Java 实现最后对照 ValidX 中BankCard/isBankCard()的真实源码讲清楚生产环境里银行卡号校验应该怎么做。一、Luhn 算法的原理1.1 算法规则Luhn 校验一个完整卡号是否有效规则只有四条从**最右侧的数字校验位**开始从右往左遍历每一位位于第 1、3、5…从右数奇数位的数字不加倍原样累加位于第 2、4、6…从右数偶数位的数字乘以 2若结果超过 9 则减去 9再累加所有位累加之和能被 10 整除则校验通过。用一句话概括“奇数位原样、偶数位加倍、超 9 减 9、总和模 10”。1.2 手工验算一经典例子 7992739871379927398713是 Luhn 算法最常见的教学示例。逐位计算从右数位置1110987654321数字79927398713处理79×218→992×2473×2698×216→771×223和 7 9 9 4 7 6 9 7 7 2 3 70 70 mod 10 0 ✅ 校验通过1.3 手工验算二中国银联卡 622202123456789462 开头是银联标识622202是工商银行借记卡前缀。6222021234567894是 16 位完整卡号最后一位 4 为校验位从右数位置16151413121110987654321数字6222021234567894处理6×212→322×242021×2223×2645×210→167×214→589×218→94和 3 2 4 2 0 2 2 2 6 4 1 6 5 8 9 4 60 60 mod 10 0 ✅ 校验通过尝试把最后一位校验位 4 改成 5和会变成 61模 10 不为 0——这就是校验位的价值错一位立刻暴露。二、Luhn 的数学本质2.1 加倍后减 9到底是什么一张双射表规则里最反直觉的一步是乘以 2 后若超过 9 就减 9。它其实是在做数字根digital root意义上的翻倍。把所有可能的输入输出列成表原数字 a0123456789f(a) 2a超 9 减 90246813579观察这张表两个关键性质f 是双射一一对应10 个输入得到 10 个互不相同的输出没有任何两个数字映射到同一个值输出恰好是0~9 中所有偶数 所有奇数的完整排列。第一条性质直接决定了检测能力见 2.4第二条性质保证了减 9和逐位相加两种写法完全等价10 → 10 1 10 − 9 1 ✔ 14 → 14 5 14 − 9 5 ✔ 18 → 18 9 18 − 9 9 ✔所以digit 9 ? digit - 9 : digit与digit / 10 digit % 10是同一个操作——这是后面代码优化的数学依据。2.2 为什么从右往左因为校验位永远在卡号最右侧。从右往左编号后校验位恰好是第 1 位不加倍它的左侧紧邻位是第 2 位加倍如此交替。这带来一个实现上的便利生成校验位和验证完整卡号可以共用同一套奇偶交替逻辑只是起始状态不同验证从不加倍开始生成从加倍开始。2.3 校验位生成算法反推已知卡号主体去掉最后一位求校验位 x。把 x 当作 0 参与计算得到主体部分的和 S则校验位 x (10 − S mod 10) mod 10以62220212345678915 位主体生成为例主体从右往左计算最右一位 9 处于加倍位置从右数位置1615141312111098765432数字622202123456789处理6×212→322×242021×2223×2645×210→167×214→589×218→9S 324202226416589 56 x (10 − 56 mod 10) mod 10 (10 − 6) mod 10 4拼上校验位得到6222021234567894——正是 1.3 节验算通过的那个号码。验证与生成互为逆运算这也是 Luhn 最优雅的地方。2.4 检测能力能抓住哪些错误参照身份证校验码那篇的分析框架把 Luhn 的检测能力精确化错误类型检测条件检测率说明单字符替换非加倍位b − a ≢ 0 (mod 10)100%b−a非零且绝对值小于 10单字符替换加倍位f(b) − f(a) ≢ 0 (mod 10)100%表 2.1 中 f 是双射不同输入必得不同输出相邻两位交换(a−b)·(权重差) ≢ 0≈98%唯一例外09 ↔ 9009 ↔ 90交换恒等于 00%见下方推导增/删一位—不一定卡号长度变化后可能碰巧仍满足校验为什么只有09 ↔ 90检测不出看 2.1 表f(0) 0f(9) 9。无论 0 和 9 谁在加倍位加倍位 0 非加倍位 9 0 9 9 加倍位 9 非加倍位 0 9 0 9 ← 交换前后贡献完全相同因为 0 和 9 恰好是 2.1 表里加倍前后不变的两个数字f(0)0、f(9)9所以它们相邻交换时总和不变。这是 Luhn 唯一的结构性盲区业界公认。对比身份证的 MOD 11-2素数模 11 让任意单字符替换 100% 可检测Luhn 用模 10 双射变换 f 达到了同样的效果但代价就是 09/90 这一个小盲区——10 是合数f 的双射性只能靠超 9 减 9这个非线性操作补回来。三、Java 实现3.1 最小验证实现把 1.1 的规则直接翻译成代码全程一次遍历O(n)publicstaticbooleanisLuhnValid(StringcardNumber){intsum0;booleandoubleDigitfalse;// 从右数第 1 位不加倍for(inticardNumber.length()-1;i0;i--){intdigitCharacter.getNumericValue(cardNumber.charAt(i));if(doubleDigit){digit*2;if(digit9){digit-9;// 超 9 减 9等价于逐位相加}}sumdigit;doubleDigit!doubleDigit;// 奇偶交替}returnsum%100;}Character.getNumericValue()而非charAt(i) - 0前者对任意 Unicode 数字字符都能正确解析后者遇到非数字字符会产生无意义结果见 3.4 的完整版进入 Luhn 之前已过滤过。3.2 校验位生成与验证共享同一套交替逻辑只是起始状态翻转校验位左侧第一位要加倍publicstaticintcalculateCheckDigit(StringcardBody){intsum0;booleandoubleDigittrue;// 校验位在最右其左侧第 1 位加倍for(inticardBody.length()-1;i0;i--){intdigitCharacter.getNumericValue(cardBody.charAt(i));if(doubleDigit){digit*2;if(digit9){digit-9;}}sumdigit;doubleDigit!doubleDigit;}return(10-sum%10)%10;// 公式x (10 − S mod 10) mod 10}用途生成测试卡号、给已有卡号补校验位、批量造数。3.3 生产级完整实现真实场景里用户输入的卡号可能带着空格“6222 0211 3400 0012”或连字符也可能包含字母。生产代码必须先清洗、再过滤、再校验publicclassBankCardValidator{publicstaticbooleanisValid(Stringvalue){if(valuenull||value.isEmpty()){returnfalse;// 空值策略由调用方决定NotNull 或其他}// 1. 去除空格与连字符6222 0211 3400 0012 → 6222021134000012Stringcleanvalue.replaceAll([\\s-],);// 2. 必须全部是数字if(!clean.matches(\\d)){returnfalse;}// 3. 长度符合 ISO/IEC 781213 ~ 19 位if(clean.length()13||clean.length()19){returnfalse;}// 4. 最后一道防线Luhn 校验returnisLuhnValid(clean);}privatestaticbooleanisLuhnValid(StringcardNumber){intsum0;booleandoubleDigitfalse;for(inticardNumber.length()-1;i0;i--){intdigitCharacter.getNumericValue(cardNumber.charAt(i));if(doubleDigit){digit*2;if(digit9){digit-9;}}sumdigit;doubleDigit!doubleDigit;}returnsum%100;}}为什么顺序是清洗 → 数字检查 → 长度检查 → Luhn每一步都在用最低成本排除上一类错误正则过滤非数字 O(n)长度比较 O(1)而 Luhn 需要完整遍历——把最贵的计算放在最后脏数据在更早的阶段就被丢弃。3.4 实现细节与常见坑坑说明正确做法charAt(i) - 0收到非数字输入含字母时产生乱值可能误判通过先matches(\\d)或直接Character.getNumericValue()忘记去空格/连字符用户习惯按 4 位一组输入replaceAll([\\s-], )预处理只做 Luhn 不做长度检查12 位数字串可能恰好满足 Luhn先校验长度 13~19把加倍后减 9写成除以 2纯属笔误但隐蔽见 2.1 表用digit 9 ? digit - 9 : digit空值直接返回true某些框架把空值交给NotNull但独立使用时易漏判明确空值策略别顺手返回 true四、完整的银行卡号校验流程Luhn 是最后一道防线不是全部。生产环境里完整的卡号校验是这样一条流水线输入 → 清洗 → 纯数字检查 → 长度检查(13~19) → BIN 前缀检查 → Luhn 校验 → 通过4.1 BIN 前缀识别发卡行卡号前 6 位是IIN/BIN发卡行识别码配合长度可以初步识别卡组织。常见的国际前缀卡组织前缀常见长度Visa4 开头16MasterCard51~5516银联6216~19American Express34、3715JCB3528~358916~19Discover6011、6516~19publicenumCardBrand{VISA,MASTERCARD,UNIONPAY,AMEX,JCB,DISCOVER,UNKNOWN;publicstaticCardBrandof(StringcardNumber){if(cardNumber.startsWith(62))returnUNIONPAY;if(cardNumber.startsWith(4))returnVISA;if(cardNumber.startsWith(34)||cardNumber.startsWith(37))returnAMEX;if(cardNumber.startsWith(51)||cardNumber.startsWith(52)||cardNumber.startsWith(53)||cardNumber.startsWith(54)||cardNumber.startsWith(55))returnMASTERCARD;if(cardNumber.matches(35(2[89]|[3-8][0-9]).*))returnJCB;// 3528~3589if(cardNumber.startsWith(6011)||cardNumber.startsWith(65))returnDISCOVER;returnUNKNOWN;}}注意BIN 识别是格式层面的启发式判断不是银行实名核验。真要验证卡号归属、余额、可用性必须走银联/银行接口。4.2 Luhn 是防错不是防伪这一点必须反复强调防错检测手误、录入错误、传输差错——这是 Luhn 的本职本地零成本完成防伪算法完全公开任何人都能对任意 15 位数字算出合法校验位。Luhn 通过 ≠ 卡真实存在更 ≠ 卡能用。所以校验通过后业务上仍需通过银行侧接口做实名/验卡。Luhn 的价值在于把绝大多数手误和无效号码挡在系统之外减少对银行接口的无效调用。五、ValidX 中的实现ValidX 把整套逻辑封装成了开箱即用的能力注解与链式 API 两种方式都支持。5.1 注解方式BankCardpublicclassBindCardDTO{NotBlank(message银行卡号不能为空)BankCard// 默认消息无效的银行卡号privateStringcardNo;}配合 Spring 的Valid自动触发。默认错误消息来自资源文件io.github.vipxieliang.validx.annotation.bank.card 无效的银行卡号支持多语言已内置中/英/日/德等 9 种。5.2 链式 APIisBankCardValidXvalidatorValidX.init().field(银行卡号).isBankCard(cardNo);if(!validator.passed()){thrownewBusinessException(validator.getErrors());}isBankCard(Object)接收Object因此Map取值、DTO 字段、方法参数都可以直接传入错误信息自动带上field()指定的字段名。5.3 源码对照ValidX内部的BankCardValidatorvalidator/financial/BankCardValidator.java与 3.3 节的生产级实现完全同构publicbooleanisValid(Stringvalue,ConstraintValidatorContextcontext){if(valuenull||value.isEmpty()){returntrue;// 空值处理交给 NotNull 等其他注解}// 1. 移除所有空格和连字符StringcleanValuevalue.replaceAll([\\s-],);// 2. 检查是否全部为数字if(!cleanValue.matches(\\d)){returnfalse;}// 3. 检查长度是否符合银行卡号规范通常为 13-19 位if(cleanValue.length()13||cleanValue.length()19){returnfalse;}// 4. 使用 Luhn 算法验证银行卡号returnisLuhnValid(cleanValue);}privatebooleanisLuhnValid(StringcardNumber){intsum0;booleanisEvenfalse;// 从右向左遍历for(inticardNumber.length()-1;i0;i--){intdigitCharacter.getNumericValue(cardNumber.charAt(i));if(isEven){digit*2;if(digit9){digit-9;}}sumdigit;isEven!isEven;}returnsum%100;}和 3.1 的最小实现逐行对应isEven就是doubleDigitdigit - 9就是超 9 减 9。注意源码中空值返回true——这是 Bean Validation 的惯例空值交给NotBlank等注解处理BankCard只负责非空时的格式与校验位。ValidX 的FinancialValidation同样封装了CVV、IBAN、SWIFT等金融类校验isBankCard与它们共用一套错误消息与字段名机制。5.4 同源应用IMEI 也用 LuhnLuhn 不止用于银行卡。手机 IMEI 是 15 位前 14 位含机型信息第 15 位就是 Luhn 校验位。ValidX 的IMEIValidator用同一套算法校验// 前 14 位偶数索引从左数第 1、3、5…位原样奇数索引从左数第 2、4、6…位加倍if(i%20){sumdigit;}else{intdoubleddigit*2;sum(doubled/10)(doubled%10);// 两位数字相加}// 校验位 (10 − sum % 10) % 10与第 15 位比对这段代码里doubled / 10 doubled % 10正是 2.1 节证明过的等价写法——与超 9 减 9完全等价。注意它与银行卡验证的方向差异卡号长度不固定13~19 位必须从右往左定奇偶IMEI 固定 15 位从右往左第 2 位即左索引 13恰好落在加倍位等价于从左数索引 1、3、5…13 加倍、索引 0、2、4…12 原样——这正是i % 2 0不加倍的原因。同一个算法银行卡和手机设备号共用。ValidX 的IMEIValidator实际兼容 15 位与 17 位17 位时取前 15 位验证后 2 位为软件版本号。六、常见问题Q1Luhn 能防伪造银行卡号吗不能。算法公开任何人可以给任意数字串算出合法校验位。Luhn 只做防错拦截手误和传输错误真正验卡必须走银行/银联接口。Q2为什么09 ↔ 90相邻交换检测不出因为f(0) 0、f(9) 9——0 和 9 是仅有的两个加倍后不变的数字。无论它们在交换中谁处于加倍位总贡献都是 9见 2.4 推导。这是 Luhn 唯一的结构性盲区。Q3Luhn 和身份证校验码MOD 11-2是一回事吗同属加权和 取模家族但设计不同身份证用素数模 11 2 的幂权重靠位置敏感检测错误Luhn 用模 10 双射变换超 9 减 9靠值变换检测错误。Luhn 更简单、更老1954身份证的 MOD 11-2ISO 7064是 1983 年标准化检测能力更强没有 09/90 盲区。Q4卡号里带空格/连字符怎么处理先replaceAll([\\s-], )清洗再校验。ValidX 的BankCard内部已自动处理。Q5怎么生成一个能通过校验的测试卡号用 3.2 节的calculateCheckDigit取 15 位前缀如测试号622202123456789算出校验位 4拼成6222021234567894。注意测试卡号只能用于本地联调不要提交到真实支付环境。Q6为什么卡号有的 13 位、有的 19 位ISO/IEC 7812 规定卡号 8~19 位主流卡组织实际使用 13~19 位。长度本身不构成校验依据但可以作为第一道廉价过滤ValidX 中为 13~19 位。七、总结把全文压缩成一条逻辑链校验位放在最右侧 │ 从右往左奇数位原样、偶数位加倍 ▼ 加倍后超 9 减 9 一张双射表 f │ 双射 → 单字符替换 100% 检测仅 09↔90 相邻交换漏检 ▼ 和 ≡ 0 (mod 10) → 校验通过 │ 验证与生成互为逆运算 ▼ Java 实现清洗 → 纯数字 → 长度(13~19) → Luhn │ ▼ ValidXBankCard / isBankCard() 已内置全流程三个必须记住的结论Luhn 是防错算法——拦截手误零成本但通过 ≠ 卡真实存在验卡仍要依赖银行接口超 9 减 9不是拍脑袋——它构造了一张双射表让偶数位加倍也能 100% 检测单字符错误代价仅是 09/90 一个盲区验证与生成是同一算法的两个方向——理解了这一点Luhn 在银行卡、IMEI、ISBN 里的各种变体就都能一眼看穿。
返回列表