最近在维护 @silenty4ng/verifyid 这个 npm 包时,我重新审视了身份证号和统一社会信用代码的校验算法,并对实现方式进行了优化。这种优化针对的是代码实现,而非算法复杂度。它利用校验算法的模运算性质,将传统的”计算校验码并比较”统一为”整体参与加权并验证模运算结果”,从而使实现更加简洁,也更便于抽象成通用的校验框架。
需要说明的是,本文并没有改变国家标准规定的校验算法,而是利用校验码满足的数学性质,将传统的「计算校验码再比较」转换为「全部字符参与加权计算并验证模运算结果」。本质上,它是一种与标准算法等价的实现方式。
一、身份证号验证
标准算法回顾
居民身份证号码(GB 11643)最后一位采用 ISO 7064:1983 MOD 11-2 校验算法,其验证过程通常如下:
取前 17 位数字;
分别乘以对应的加权因子:
1
[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
将乘积求和得到
S;计算
S % 11;根据余数查表:
1
[1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2]
比较得到的校验码是否与第 18 位一致。
数学性质
ISO 7064 MOD 11-2 的加权因子可表示为:
1 | Wi = 2^(17-i) mod 11 |
展开后正好得到前 17 位使用的权重:
1 | 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2 |
继续推导最后一位可得:
1 | W18 = 2^0 mod 11 = 1 |
因此,校验位对应的权重天然就是 1,并非额外人为添加。
将身份证校验码视为数值,其中:
0~9对应0~9X对应10
设:
1 | S = 前17位加权和 |
根据标准算法:
1 | C = (12 - (S % 11)) % 11 |
令:
1 | r = S % 11 |
则有:
1 | C = (12 - r) % 11 |
于是:
1 | (S + C) % 11 |
由于校验位权重为 1,因此可以直接让 18 位全部参与加权计算:
1 | sum = |
最终只需判断:
1 | sum % 11 === 1 |
即可完成校验。
实现代码
1 | function validateIdCard(idCard) { |
实现特点
- 使用正则表达式完成基本格式验证;
- 利用 JavaScript
Date对象自动归一化(溢出)的特性验证日期是否合法,例如2026-02-30会自动变为2026-03-02,从而可以检测非法日期(这种写法依赖 JavaScript 的Date实现,在其他语言中未必适用); - 不再需要根据余数查找校验码,而是直接验证整个号码是否满足模运算条件。
二、统一社会信用代码验证
标准算法回顾
统一社会信用代码(GB/T 32100-2015)采用 31 进制校验算法。
通常流程如下:
取前 17 位字符;
将字符转换为对应数值:
1
0123456789ABCDEFGHJKLMNPQRTUWXY
共 31 个字符,其中不包含:
1
I O S V Z
分别乘以权重:
1
[1,3,9,27,19,26,16,17,20,29,25,13,8,24,10,30,28]
求和得到
S;计算:
1
C = (31 - (S % 31)) % 31
将
C映射为校验字符并比较。
数学性质
GB/T 32100-2015 的权重可表示为:
1 | Wi = 3^(17-i) mod 31 |
展开后得到:
1 | 1 3 9 27 19 26 16 17 20 29 25 13 8 24 10 30 28 |
继续推导最后一位:
1 | W18 = 3^0 mod 31 = 1 |
因此,第 18 位校验字符对应的权重同样天然为 1。
设:
1 | S = 前17位加权和 |
根据标准算法:
1 | C = (31 - (S % 31)) % 31 |
令:
1 | r = S % 31 |
则:
1 | C = (31 - r) % 31 |
因此:
1 | (S + C) % 31 |
于是可以直接把第 18 位一起参与计算:
1 | sum = |
最后只需判断:
1 | sum % 31 === 0 |
即可完成校验,而无需再单独计算校验字符。
实现代码
1 | function validateCreditCode(code) { |
实现特点
- 正则表达式直接排除非法字符;
- 不再需要根据余数计算最终校验字符,只需验证整体是否满足模运算条件;
- 正则与字符映射双重校验,保证输入合法性。
如果希望避免重复线性扫描字符集,可以预先构建字符到数值的映射表(如
Map或对象字典),将查找变为常数时间。不过由于字符集仅有 31 个字符,两者在实际场景中的性能差异几乎可以忽略。
三、总结
通过分析两种校验算法,可以发现它们都具有相同的数学特性。
| 校验对象 | 传统实现 | 等价实现 |
|---|---|---|
| 身份证号 | 前17位计算校验码,再比较第18位 | 18 位全部参与计算,判断 sum % 11 === 1 |
| 统一社会信用代码 | 前17位计算校验码,再比较第18位 | 18 位全部参与计算,判断 sum % 31 === 0 |
这种实现方式并没有改变标准算法,而是利用模运算的不变量,将”计算校验码并比较”转换为”整体参与加权并验证模值”。
从工程角度来看,它减少了校验码映射步骤,使代码更加统一、简洁,也更容易抽象成通用的校验框架。
值得注意的是,这种思路并非身份证号和统一社会信用代码所独有。对于许多采用线性加权 + 模运算生成校验位的编码体系,只要校验位对应的权重为 1,且校验码由整体模约束推导得到,就可以将”计算校验码再比较”转换为”整体参与加权并验证模值”。本文介绍的两种算法正是这一思想在国家标准中的具体应用。
目前这一实现已经应用于 @silenty4ng/verifyid 中,所有测试用例均已通过,包括各种边界情况和标准示例。
如果你对本文的推导或实现有不同的理解,欢迎一起讨论,也欢迎在 GitHub 上提交 Issue 或 PR。