身份证号与统一社会信用代码校验算法的等价实现

最近在维护 @silenty4ng/verifyid 这个 npm 包时,我重新审视了身份证号和统一社会信用代码的校验算法,并对实现方式进行了优化。这种优化针对的是代码实现,而非算法复杂度。它利用校验算法的模运算性质,将传统的”计算校验码并比较”统一为”整体参与加权并验证模运算结果”,从而使实现更加简洁,也更便于抽象成通用的校验框架。

需要说明的是,本文并没有改变国家标准规定的校验算法,而是利用校验码满足的数学性质,将传统的「计算校验码再比较」转换为「全部字符参与加权计算并验证模运算结果」。本质上,它是一种与标准算法等价的实现方式。

一、身份证号验证

标准算法回顾

居民身份证号码(GB 11643)最后一位采用 ISO 7064:1983 MOD 11-2 校验算法,其验证过程通常如下:

  1. 取前 17 位数字;

  2. 分别乘以对应的加权因子:

    1
    [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  3. 将乘积求和得到 S

  4. 计算 S % 11

  5. 根据余数查表:

    1
    [1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2]
  6. 比较得到的校验码是否与第 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~9
  • X 对应 10

设:

1
2
S = 前17位加权和
C = 校验码数值(X = 10)

根据标准算法:

1
C = (12 - (S % 11)) % 11

令:

1
r = S % 11

则有:

1
C = (12 - r) % 11

于是:

1
2
3
4
5
6
7
(S + C) % 11
=
(r + C) % 11
=
(r + 12 - r) % 11
=
1

由于校验位权重为 1,因此可以直接让 18 位全部参与加权计算

1
2
3
sum =
前17位加权和 +
校验码 × 1

最终只需判断:

1
sum % 11 === 1

即可完成校验。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function validateIdCard(idCard) {
if (typeof idCard !== 'string') {
return false;
}

idCard = idCard.toUpperCase().trim();

// 格式验证(为避免明显异常数据,本实现将出生年份限制为 1800~2099,可根据业务需要调整。)
const reg =
/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/;

if (!reg.test(idCard)) {
return false;
}

// 验证日期是否合法
const year = Number(idCard.slice(6, 10));
const month = Number(idCard.slice(10, 12));
const day = Number(idCard.slice(12, 14));

const date = new Date(year, month - 1, day);

if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day
) {
return false;
}

const weights = [
7, 9, 10, 5, 8, 4, 2, 1,
6, 3, 7, 9, 10, 5, 8, 4,
2, 1
];

let sum = 0;

for (let i = 0; i < 18; i++) {
const value = idCard[i] === 'X' ? 10 : Number(idCard[i]);
sum += value * weights[i];
}

return sum % 11 === 1;
}

实现特点

  • 使用正则表达式完成基本格式验证;
  • 利用 JavaScript Date 对象自动归一化(溢出)的特性验证日期是否合法,例如 2026-02-30 会自动变为 2026-03-02,从而可以检测非法日期(这种写法依赖 JavaScript 的 Date 实现,在其他语言中未必适用);
  • 不再需要根据余数查找校验码,而是直接验证整个号码是否满足模运算条件。

二、统一社会信用代码验证

标准算法回顾

统一社会信用代码(GB/T 32100-2015)采用 31 进制校验算法。

通常流程如下:

  1. 取前 17 位字符;

  2. 将字符转换为对应数值:

    1
    0123456789ABCDEFGHJKLMNPQRTUWXY

    共 31 个字符,其中不包含:

    1
    I O S V Z
  3. 分别乘以权重:

    1
    [1,3,9,27,19,26,16,17,20,29,25,13,8,24,10,30,28]
  4. 求和得到 S

  5. 计算:

    1
    C = (31 - (S % 31)) % 31
  6. 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
2
S = 前17位加权和
C = 校验字符对应数值

根据标准算法:

1
C = (31 - (S % 31)) % 31

令:

1
r = S % 31

则:

1
C = (31 - r) % 31

因此:

1
2
3
4
5
6
7
(S + C) % 31
=
(r + C) % 31
=
(r + 31 - r) % 31
=
0

于是可以直接把第 18 位一起参与计算:

1
2
3
sum =
前17位加权和 +
校验码 × 1

最后只需判断:

1
sum % 31 === 0

即可完成校验,而无需再单独计算校验字符。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function validateCreditCode(code) {
if (typeof code !== 'string') {
return false;
}

code = code.toUpperCase().trim();

const reg = /^[0-9A-HJ-NPQRTUWXY]{18}$/;

if (!reg.test(code)) {
return false;
}

const chars = '0123456789ABCDEFGHJKLMNPQRTUWXY';

const weights = [
1, 3, 9, 27, 19, 26, 16, 17,
20, 29, 25, 13, 8, 24, 10,
30, 28, 1
];

let sum = 0;

for (let i = 0; i < 18; i++) {
const value = chars.indexOf(code[i]);

if (value === -1) {
return false;
}

sum += value * weights[i];
}

return sum % 31 === 0;
}

实现特点

  • 正则表达式直接排除非法字符;
  • 不再需要根据余数计算最终校验字符,只需验证整体是否满足模运算条件;
  • 正则与字符映射双重校验,保证输入合法性。

如果希望避免重复线性扫描字符集,可以预先构建字符到数值的映射表(如 Map 或对象字典),将查找变为常数时间。不过由于字符集仅有 31 个字符,两者在实际场景中的性能差异几乎可以忽略。


三、总结

通过分析两种校验算法,可以发现它们都具有相同的数学特性。

校验对象 传统实现 等价实现
身份证号 前17位计算校验码,再比较第18位 18 位全部参与计算,判断 sum % 11 === 1
统一社会信用代码 前17位计算校验码,再比较第18位 18 位全部参与计算,判断 sum % 31 === 0

这种实现方式并没有改变标准算法,而是利用模运算的不变量,将”计算校验码并比较”转换为”整体参与加权并验证模值”。

从工程角度来看,它减少了校验码映射步骤,使代码更加统一、简洁,也更容易抽象成通用的校验框架。

值得注意的是,这种思路并非身份证号和统一社会信用代码所独有。对于许多采用线性加权 + 模运算生成校验位的编码体系,只要校验位对应的权重为 1,且校验码由整体模约束推导得到,就可以将”计算校验码再比较”转换为”整体参与加权并验证模值”。本文介绍的两种算法正是这一思想在国家标准中的具体应用。

目前这一实现已经应用于 @silenty4ng/verifyid 中,所有测试用例均已通过,包括各种边界情况和标准示例。

如果你对本文的推导或实现有不同的理解,欢迎一起讨论,也欢迎在 GitHub 上提交 Issue 或 PR。

https://github.com/silenty4ng/verifyid