ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ValidX 银行卡BIN码识别:如何判断银行和卡种

ValidX 银行卡BIN码识别:如何判断银行和卡种

银行卡BIN码识别:如何判断银行和卡种

📋 目录

  • 引言
  • 一、BIN 码是什么
  • 二、银行卡号的结构:MII + BIN + 账号 + 校验位
  • 三、如何判断卡组织
  • 四、如何判断银行和卡种
  • 五、匹配实现:四种算法选型
  • 六、与 ValidX 结合:校验与识别各司其职
  • 七、实战:一个完整的 BIN 识别工具类
  • 八、边界与坑
  • 总结

引言

支付表单、绑卡页面、收银台……几乎每个金融场景都要面对一张银行卡号。

输入框里敲下 16 位卡号,你往往需要立刻知道三件事:

  1. 卡号格式对不对?是不是手滑多敲了一位(Luhn 校验位兜底)
  2. 哪个银行发的?显示"中国工商银行"而不是"未知卡号"
  3. 什么卡种?借记卡还是信用卡?Visa、MasterCard 还是银联?

第 1 件事靠Luhn 算法(ValidX 的@BankCard已内置),第 2、3 件事靠BIN 码识别。本文拆解 BIN 码的原理与实现,并给出与 ValidX 组合使用的完整方案。


一、BIN 码是什么

BIN(Bank Identification Number,银行识别码)是银行卡号的开头6 位数字,用来标识发卡机构。它由 ISO 组织统一分配,标准依据是ISO/IEC 7812。

概念全称位数作用
MIIMajor Industry Identifier(主要行业标识符)1 位标识发卡行业(金融/航空/旅游…)
IIN / BINIssuer Identification Number(发卡机构标识)6 位标识具体发卡行
个人账号Individual Account Number6-12 位银行内部账号
校验位Check Digit1 位Luhn 算法校验

看一张真实卡号6228480402564890018的拆解:

622848 040256489001 8 └──┬───┘ └─────┬──────┘ └───── 校验位(Luhn) │ └─────────────── 银行内部账号(可变长度,第 7-18 位) └─────────────────────────── BIN(前 6 位 = 农行金穗借记卡)

注意:MII 是 BIN 的第 1 位,而不是独立于 BIN 之外的一段。622848这 6 位整体由银联分配给农行,识别时直接取卡号前 6 位查表即可,不必单独解析 MII(本例首字符6是 MII,表示金融业 / 银联,已包含在 BIN 内)。

注:BIN 严格定义是前 6 位,但 2017 年起 ISO 标准允许 IIN 扩展到 8 位。国内银联卡实际分配以 6 位为主,识别时取前 6 位即可覆盖绝大多数场景。

为什么是"前 6 位"?因为发卡行给卡号编号时有严格的前缀分配制度——每家银行的每种卡产品都锁定了固定的 BIN 段。比如农行的金穗借记卡固定以622848开头,看到这 6 位就知道是农行。


二、银行卡号的结构:MII + BIN + 账号 + 校验位

对照上面那张拆解图,逐段说清楚:

① MII(第 1 位)—— 先判断大行业

ISO/IEC 7812 规定了第一位数字的行业归属:

MII行业代表卡组织
1、2航空业UATP
3旅游、娱乐Amex(34/37)、Diners(36/38)
4银行业Visa
5银行业MasterCard(51-55)
6银行业、商品Discover、中国银联(62)
9国内分配银联早期 955 段

看到4开头就知道大概率是 Visa,看到62开头就是银联——这就是"先按 MII 分行业,再按 BIN 分机构"的两级识别思路。

② BIN(第 2-7 位)—— 锁死发卡行和卡种

同一家银行的不同 BIN 段代表不同卡产品,这是"卡种识别"的根据。例如:

  • 工行622202→ 牡丹灵通卡(借记卡)
  • 工行622230→ 牡丹信用卡(贷记卡)
  • 招行622588→ 一卡通(借记卡)
  • 招行439188→ 信用卡(Visa)

③ 账号 + 校验位 —— 银行内部逻辑,识别时不用关心,但校验位必须参与格式校验(Luhn)。


三、如何判断卡组织

只靠前 2-4 位就能判断卡组织(卡品牌),这是 BIN 识别最基础的一层:

publicenumCardBrand{VISA,MASTERCARD,AMEX,DINERS,JCB,UNIONPAY,DISCOVER,UNKNOWN}publicstaticCardBranddetectBrand(StringcardNumber){if(cardNumber==null)returnCardBrand.UNKNOWN;Stringp=cardNumber.replaceAll("[\\s-]","");if(p.startsWith("4"))returnCardBrand.VISA;// 4 开头if(p.startsWith("51")||p.startsWith("52")||p.startsWith("53")||p.startsWith("54")||p.startsWith("55"))returnCardBrand.MASTERCARD;// 51-55if(p.startsWith("34")||p.startsWith("37"))returnCardBrand.AMEX;// 34/37if(p.startsWith("36")||p.startsWith("38"))returnCardBrand.DINERS;// 36/38if(p.startsWith("35"))returnCardBrand.JCB;// 35if(p.startsWith("62"))returnCardBrand.UNIONPAY;// 62 银联if(p.startsWith("6011")||p.startsWith("65"))returnCardBrand.DISCOVER;returnCardBrand.UNKNOWN;}

关键规律只有一条:银联 =62开头(16-19 位)。判断银联卡,startsWith("62")就够了。


四、如何判断银行和卡种

卡组织是"大标签",银行和卡种是"精确标签"。这层就要靠BIN 前缀表。

1. 常见银行 BIN 速查表

以下是中国大陆常见银行的借记卡 BIN 前缀(示例,非完整清单):

银行常见 BIN 前缀卡种示例
中国工商银行622202622200牡丹灵通卡(借记卡)
中国建设银行621700622700436742龙卡通(借记卡)
中国农业银行622848622845金穗借记卡
中国银行621661622260622262长城电子借记卡
招商银行622588621485一卡通(借记卡)
交通银行622258622259太平洋卡(借记卡)
中国邮政储蓄银行621098622188绿卡(借记卡)
兴业银行622908622909借记卡
浦发银行622521622522借记卡
中信银行622690622691借记卡
民生银行622622622623借记卡
光大银行622662622663借记卡
平安银行622155622156借记卡

⚠️ 表中前缀仅用于理解原理。真实生产环境必须维护完整 BIN 库(可用银联官方 BIN 表,约数万条记录),否则会大量误判。

2. 借记卡 vs 信用卡:怎么区分

卡种识别不能靠卡号长度(借记卡和信用卡都是 16/19 位,长度无法区分),必须查 BIN 表。BIN 表每条记录应包含"卡品牌 + 卡种"两个字段:

// 同一银行的两个 BIN,卡种不同622202→ 工商银行/借记卡(牡丹灵通卡)622230→ 工商银行/贷记卡(牡丹信用卡)

卡号6222021234567890和6222301234567890只差 6 位,卡种截然不同——这就是"为什么要精确匹配 BIN 而不是匹配前 4 位"的原因。

3. 匹配规则:最长前缀优先

622848同时是62、6228、622848的前缀,识别时必须取最长匹配,否则会把农行卡误判成"银联/未知银行":

// 错误:先匹配 62 就停 → 只能判断出"银联"// 正确:遍历所有 BIN,取能匹配的最长前缀 → 判断出"农行金穗卡"

五、匹配实现:四种算法选型

BIN 表规模从小到大的四种实现:

方案适用规模复杂度特点
线性遍历< 100 条O(n)最简单,正则逐条匹配,适合教学
HashMap 精确匹配任意O(1)将卡号前 6 位截出直接查表,仅适用等长 BIN
前缀树(Trie)> 1000 条O(k),k=卡号长度天然支持变长前缀,取最长匹配,工业首选
二分查找排序表O(log n)前缀区间排序后二分,省内存

工业实现推荐 Trie 或 HashMap。银联官方 BIN 表约 4 万条,Trie 的查询耗时与 BIN 数量无关(只跟卡号长度有关),且天然解决"最长前缀"问题。

HashMap 版本最直观(BIN 等长时):

Map<String,BankInfo>binMap=newHashMap<>();binMap.put("622848",newBankInfo("中国农业银行","借记卡"));// 识别:截前 6 位查表Stringbin=cleanValue.substring(0,Math.min(6,cleanValue.length()));BankInfoinfo=binMap.get(bin);

Trie 版本的核心思路:把每个 BIN 作为路径插入字典树,查询时沿卡号逐字符向下走,记录最后一个命中银行节点的深度,即为最长匹配。


六、与 ValidX 结合:校验与识别各司其职

很多人会混淆两件事:

任务算法ValidX 对应能力
卡号格式校验(是否可被发卡行接受)Luhn 校验位 + 长度 + 纯数字@BankCard注解 / 链式isBankCard()
银行卡种识别(哪家银行、什么卡)BIN 前缀匹配无内置 BIN 库,需自定义实现

ValidX 的@BankCard只做校验、不做识别,看源码(BankCardValidator.java)就明白:

publicbooleanisValid(Stringvalue,ConstraintValidatorContextcontext){if(value==null||value.isEmpty()){returntrue;// 空值处理交给@NotNull等其他注解处理}// 移除所有空格和连字符StringcleanValue=value.replaceAll("[\\s-]","");// 检查是否全部为数字if(!cleanValue.matches("\\d+")){returnfalse;}// 检查长度是否符合银行卡号规范(通常为13-19位)if(cleanValue.length()<13||cleanValue.length()>19){returnfalse;}// 使用Luhn算法验证银行卡号returnisLuhnValid(cleanValue);}

处理流程清晰:去空格连字符 → 纯数字 → 13-19 位 → Luhn。校验通过不代表卡一定存在,但校验失败一定无效——这是 BIN 识别的前置关卡。

正确的组合链路是先校验、后识别:

用户输入卡号 │ ▼ ① @BankCard / isBankCard() ──失败──→ 提示"卡号格式不正确" │ 通过 ▼ ② BIN 识别(自定义) ──未知──→ 显示"该卡号暂不支持" │ 命中 ▼ ③ 返回 { 银行, 卡种, 卡组织 }

校验保证"号码本身合法",识别负责"告诉用户这是什么卡",两者互补、缺一不可。


七、实战:一个完整的 BIN 识别工具类

结合 ValidX 链式 API,给出一个可直接运行的完整示例:

importio.github.vipxieliang.validx.chain.ValidX;importjava.util.HashMap;importjava.util.Map;/** * 银行卡识别工具:先 Luhn 校验,再 BIN 识别 */publicclassBankCardInfoUtil{/** BIN 表:前缀 -> (银行, 卡种, 卡组织) */privatestaticfinalMap<String,CardInfo>BIN_TABLE=newHashMap<>();static{BIN_TABLE.put("622202",newCardInfo("中国工商银行","借记卡","银联"));BIN_TABLE.put("622230",newCardInfo("中国工商银行","贷记卡","银联"));BIN_TABLE.put("621700",newCardInfo("中国建设银行","借记卡","银联"));BIN_TABLE.put("622848",newCardInfo("中国农业银行","借记卡","银联"));BIN_TABLE.put("622588",newCardInfo("招商银行","借记卡","银联"));BIN_TABLE.put("621098",newCardInfo("中国邮政储蓄银行","借记卡","银联"));BIN_TABLE.put("401288",newCardInfo("Visa 测试卡","贷记卡","Visa"));BIN_TABLE.put("542523",newCardInfo("MasterCard 测试卡","贷记卡","MasterCard"));}publicstaticCardInfoparse(StringcardNumber){// ① 先用 ValidX 做 Luhn 校验,不合法直接返回 nullif(!ValidX.init().isBankCard(cardNumber).passed()){returnnull;}// ② 归一化后取前 6 位查 BIN 表Stringclean=cardNumber.replaceAll("[\\s-]","");Stringbin=clean.substring(0,Math.min(6,clean.length()));returnBIN_TABLE.get(bin);}publicstaticvoidmain(String[]args){// 合法且 BIN 命中System.out.println(parse("6228480402564890018"));// 中国农业银行/借记卡/银联// 合法但 BIN 未收录(622700 开头,表中无此段)System.out.println(parse("622700888888888882"));// null(Luhn 通过,BIN 表没有)// Luhn 校验失败(末位校验位数字 8 误写成 9)System.out.println(parse("6228480402564890019"));// null// 长度不足System.out.println(parse("12345"));// null}staticclassCardInfo{finalStringbankName,cardType,brand;CardInfo(StringbankName,StringcardType,Stringbrand){this.bankName=bankName;this.cardType=cardType;this.brand=brand;}@OverridepublicStringtoString(){returnbankName+"/"+cardType+"/"+brand;}}}

示例输出:

中国农业银行/借记卡/银联 null null null

三个关键点:

  1. isBankCard()返回ValidX对象,passed()判断结果——链式 API 天然适合"先校验后识别"的编排
  2. 识别失败返回null,前端可降级显示"该卡号暂不支持",而不是直接拒绝——BIN 表永远在增长,别把"未收录"当"非法"
  3. 测试卡号也放进 BIN 表:401288(Visa 测试)、542523(MasterCard 测试)与 ValidX 测试用例中的卡号呼应,便于联调

进阶:注解方式识别

BIN 识别不依赖 Bean Validation,但你可以把它包成自定义注解,与@BankCard叠加使用:

@BankCard@BankCardInfo(bank="中国农业银行")// 自定义约束:Luhn 通过后再查 BINprivateStringcardNumber;

自定义注解的ConstraintValidator内部直接调用BankCardInfoUtil.parse()判断银行是否匹配,@BankCard与@BankCardInfo各自职责单一,任一失败都会产生违规。


八、边界与坑

1. BIN 表有时效性,必须定期更新

新卡种、新 BIN 段每年都在增加,一次入库管终身必然误判。生产建议:银联官方 BIN 表 + 每月增量更新 + 未命中时记录日志供分析。

2. 别用"卡号长度"判断卡种

16 位既有借记卡也有信用卡,长度判断必然出错。卡种只能查 BIN 表。

3. 最长前缀匹配

622848同时命中62(银联)、6228、622848(农行),必须取最长。用 Trie 或"先排长度降序再逐条 startsWith"都能解决。

4. 空格、连字符必须先行归一化

用户输入的卡号常带-或空格(ValidX 测试用例"5425-2334-3010-9903"就是典型),识别前必须replaceAll("[\\s-]", "")。注意顺序:先归一化,再取 BIN。

5. 测试卡号与真实卡号

4012888888881881(Visa 官方测试卡)、5555555555554444(MasterCard 官方测试卡)、4532015112830366(常见支付网关测试号)等是公开测试卡号,Luhn 合法但没有真实银行账户。别把它们当"有效卡"放行支付,BIN 识别应加"测试卡号"黑名单。

6. “未收录"不等于"非法”

BIN 表命中失败,应提示"暂不支持该卡"而非"卡号错误"。Luhn 校验失败才是"卡号错误"——两件事信号含义不同,错误提示也别混。

7. 私有 BIN 与预发卡

部分银行内部卡(如特定行业卡、内部结算卡)的 BIN 不在公开表内。如果业务需要覆盖,需向银行或银联申请私有 BIN 段说明。


总结

层级输入输出算法ValidX 支持
卡组织识别卡号前 2-4 位Visa / MasterCard / 银联…MII + 前缀规则❌(需自定义,极简单)
银行识别卡号前 6 位具体银行BIN 前缀表匹配❌(需自定义 + BIN 库)
卡种识别BIN 表记录借记卡 / 贷记卡BIN 表字段查询❌(需自定义 + BIN 库)
格式校验整张卡号是否合法Luhn + 长度 + 纯数字✅@BankCard/isBankCard()

一句话总结:

  • @BankCard回答"这个号码写对没有"(Luhn 校验,ValidX 已内置)
  • BIN 识别回答"这是什么卡"(前缀匹配,自己维护 BIN 表)

两者组合,就能在绑卡页面上做到"输入即识别、错误即拦截"的完整体验。BIN 识别本身不复杂——本质就是截前 6 位、查表、取最长前缀,真正的工程量在于维护一份完整且新鲜的 BIN 库。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进