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

最近在维护 @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

在服务器使用 Docker 部署 CoPaw

安装 Docker 和 防火墙

1
apt install podman ufw

配置防火墙

在 /etc/ufw/after.rules 增加以下配置

1
2
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN

运行防火墙

1
2
ufw allow 22
ufw enable

运行 CoPaw 容器

1
2
3
mkdir copaw-data
mkdir copaw-secrets
podman run --privileged --name copaw --restart=always -d --network=host -v ./copaw-data:/app/working -v ./copaw-secrets:/app/working.secret docker.io/agentscope/copaw:latest

进行初始化配置(工作区文件)

1
2
podman exec -it copaw bash
copaw init --defaults

连接到 Web 控制台

1
ssh -N -L 8088:127.0.0.1:8088 root@ip

访问 http://127.0.0.1:8088 配置模型及频道。

2026 新年快乐

新年快乐

临近旧历新年,祝大家马年新年快乐,在新年里不劳而获,无功受禄。

2025 年总结词

世界和平

一些开心的事

  • 感谢合作伙伴
  • 感谢事业伙伴

2025 新年快乐

新年快乐

旧历新年,祝大家蛇年新年快乐,在新年里不劳而获,无功受禄。

2024 年总结词

踉跄

一些开心的事

  • 感谢业余无线电小伙伴们
  • 尝试钢琴

互联网不存在了

近期我的网页无线电台写频小工具 K5Web 网站上线,我通过更改 CDN 等方式试图让国内所有地方都能够访问,但总有一两个地方的反诈系统会拦截我的网站;我想因为网络主权问题,屏蔽我这个境外域名的访问也属于是维护主权了,于是在国内购买域名和服务器,完成备案在国内上线了 K5Web ,没想到还有地方会拦截我的网站,这种似乎白名单的治理模式我真不知道怎么处理,如果这些地方的无线电爱好者没法访问,那我也没什么可以帮到你们的方法。

昨天看到新闻说马来西亚计划开发互联网“关闭开关”,之前也听说过俄罗斯的断网演习,彻底断开互联网似乎是未来世界网络主权模式下的可行方案,互联网越来越缺少团结。

我之前拥有过不少国家的 VPS,也通过这些国家的 VPS 访问互联网,在美国、英国、法国、德国等都会有各自屏蔽的网站列表(主要针对盗版内容,大部分是通过 DNS 劫持屏蔽)无法访问,基于黑名单的网络主权治理是当前比较流行的方案。

IPv6 网络的糟糕互联也是“互联网不存在”的一大问题,COGENT 公司与 HE 公司的 IPv6 网络至今没有互联,因为他们之间的冲突,如果选用其中一家的网络服务,就无法直接联通到另一个网络服务商的网络。

在我看来,未来接入互联网需要在每个国家、地区、运营商都进行登记才能访问也不是没有可能,互联网不存在了。

不要再看萨特的书了

普通人奇怪哲学家总是想那些大而无当的问题
而哲学家又奇怪
普通人只关注小事而不想大事
到底谁有理
我认为关键在灵魂是否认真
即使普通人
你的灵魂是认真的
你就一定会想大问题
因为那不是抽象的理论问题
而是你最切身的问题
当一个人的灵魂
对人生产生根本性的疑问时
他就会走向哲学
那当你不是逃避
而是去正视和思考让你疑惑的重大问题时
你的灵魂就醒了
你就进入了哲学的状态

乐观主义的人是浅薄的人
但经由悲观主义而产生的乐观主义者
则是体验过人生忧患以后
重新肯定人生的人
一个有深度的人
当他最初接触人生世界时
一定会发现这个世界是一个痛苦的世界
一定对人生世界产生悲观的看法
如果他沉沦在悲观主义里面
他就会颓废
如果他不沉沦在悲观主义里面
而能跳出来的话
他就会重新肯定人生世界
对人生世界充满乐观的希望

旧历新年快乐

2024 新年快乐

新年快乐

新历新年,给大家说一个 2024 年新年快乐,在新年里开开心心,事事顺遂。

2023 年总结词

迷茫

一些开心的事

  • 离开不开心的工作。
  • DN42网络运行正常。(AS4242420706
  • 拿到了业余无线电呼号。(BD8DFN
  • 在家运行一个 NAS。

DoH 服务器的 Python 实现

实现

看到有 Go、PHP 语言实现的 DoH 服务器,于是弄了个 Python 实现的 DoH 服务器(兼容 RFC8484),可以使用 Nginx、Caddy 反向代理 /dns-query 接口完成部署。

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
"""
/*
* @Author: Silent YANG
* @Date: 2023-11-29
*/
"""

from flask import Flask, request, make_response
from base64 import urlsafe_b64decode
import socket

app = Flask(__name__)
socket.setdefaulttimeout(5)

@app.route("/dns-query" ,methods=['GET', 'POST'])
def dns_query():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
if request.headers.get('CONTENT_TYPE') == 'application/dns-message':
sock.sendto(request.stream.read(), ("127.0.0.1", 53))
rx_meesage, addr = sock.recvfrom(4096)
return make_response(rx_meesage, 200, {"Content-Type": "application/dns-message"})
if request.method == "GET" and request.args.get("dns"):
payload = urlsafe_b64decode(request.args.get("dns") + "===")
sock.sendto(payload, ("127.0.0.1", 53))
rx_meesage, addr = sock.recvfrom(4096)
return make_response(rx_meesage, 200, {"Content-Type": "application/dns-message"})
sock.close()
return make_response("", 400)

if __name__ == "__main__":
app.run('127.0.0.1', debug=False, port=5000)

完整代码

https://github.com/silenty4ng/DoH

将博客迁移到 NAS

我的博客已经迁移到了我家 NAS,现在使用 Hexo。

本来尝试使用 WordPress,但 WordPress 在 NAS 容器中性能很差,又切换到了 Hexo。