中国公民身份证号码验证标准、方案与实现

身份证号码验证在日常项目中是一个经常性的需求,本文结合相关标准及个人经验,罗列出了在验证时需考虑的几个点,最后给出以 TypeScript 实现的代码。

身份证号码标准

溯本追源乃解决问题的根本大法。在 全国标准信息公共服务平台 上,我找到了该标准:GB 11643-1999:公民身份号码

标准对身份证号码结构的表述是这样的:

公民身份证号码是特征组合码,由十七位数字本体码和一位数字校验码组成。排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。

举个例子,一女性公民的身份证号码是 11010519491231002X,则其含义如下:

地址码出生日期码顺序码校验码
11010519491231002X

其中:

地址码

表示身份证持有人户口所在县(市、旗、区)的行政区划代码,其标准为:GB/T 2260-2007:中华人民共和国行政区划代码

根据标准,我国行政区划代码为 三层六位 的结构:

  • 第一层:前面两位,表示省、自治区、直辖市、特别行政区,取值为:11, 12, 13, 14, 15, 21, 22, 23, 31, 32, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 54, 61, 62, 63, 64, 65, 71, 81, 82,其中 11~65北京到新疆 的编码,71, 81, 82 分别为 台湾香港澳门 的编码。
  • 第二层:中间两位,表示市、地区、自治州、盟等。
  • 第三层:后面两位,表示县、自治县、县级市、旗等。

出生日期码

表示身份证持有人的出生年、月、日,其标准为:GB/T 7408-2005:数据元和交换格式 信息交换 日期和时间表示法

简言之,年、月、日之间不用分隔符,年为四位数字,月、日都为两位数字。

顺序码

表示在同一地址码所标识的区域范围内,对同年、同月、同日出生的人编定的顺序号,顺序码的奇数分配给男性,偶数分配给女性。

比如我的身份证号码顺序码为 531,表明至少还有 264 个人也是在我出生那天降临于世的,同时这是一个奇数,说明我是男孩子。

校验码

校验码采用 ISO 7064:1983.MOD11-2 标准。

计算过程如下:

1. 对前 17 位本体码加权求和

S = Sum(Ai * Wi)i = 0, ..., 16

Ai:表示第 i 位置上的身份证号码数字值

Wi:表示第 i 位置上的加权因子,Wi = 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2

2. 计算模

Y = mod(S, 11)

3. 通过模得到对应的校验码

模值Y:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

校验码:1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2

比如,模值为 2,则校验码为 X。

验证方案

有了上面的说明,就能很轻松地写出身份证号码的验证方案了。

  • 首位不为 0 的 18 位字符串,由 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, X 字符组成
  • 前两位地区码在 11, 12, 13, 14, 15, 21, 22, 23, 31, 32, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 54, 61, 62, 63, 64, 65, 71, 81, 82 之中
  • 出生日期有效,且小于当前日期
  • 对前 17 位数字做校验码计算的结果等于第 18 位上的字符

另外,还应考虑一些特殊情况:

  • 是否兼容 15 位的身份证号码
  • X 与 x 的大小写兼容

基于 TypeScript 的实现

方案有了,实现也就简单了。下面是一个兼容 15 位的身份证号验证函数,其被发布在了我开源的工具库 vtils 上,你可通过 npm i vtils,然后 import { isChineseIDCardNumber } from 'vtils' 使用,查看源码

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
46
47
48
49
50
51
52
53
54
55
56
57
const testRegExp = /^[1-9]([0-9]{14}|[0-9]{16}[0-9Xx])$/
const areaMap = [11, 12, 13, 14, 15, 21, 22, 23, 31, 32, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 54, 61, 62, 63, 64, 65, 71, 81, 82]
const weightMap = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const codeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']

const isValidDate = (year: number, month: number, day: number): boolean => {
const date = new Date(year, month - 1, day)
return (
date.getFullYear() === year
&& date.getMonth() + 1 === month
&& date.getDate() === day
&& date.getTime() < new Date().getTime()
)
}

/**
* 检测 `value` 是否是中国大陆身份证号码。
*
* @param value 要检测的值
* @returns 是(true)或否(false)
* @see https://my.oschina.net/labrusca/blog/306116
* @see http://developer.51cto.com/art/201803/568755.htm
*/
export default function isChineseIDCardNumber(value: string): boolean {
const len = value.length

// 长度错误
if (len !== 15 && len !== 18) {
return false
}

// 模式校验
if (!testRegExp.test(value)) {
return false
}

// 地区校验
if (areaMap.indexOf(+value.substr(0, 2)) === -1) {
return false
}

// 15 位
if (len === 15) {
return isValidDate(+`19${value.substr(6, 2)}`, +value.substr(8, 2), +value.substr(10, 2))
}

// 18 位
if (!isValidDate(+value.substr(6, 4), +value.substr(10, 2), +value.substr(12, 2))) {
return false
}

// 校验码
const sum = value.split('').slice(0, 17).reduce((s, num, index) => {
return s += +num * weightMap[index]
}, 0)
return codeMap[sum % 11] === value[17].toUpperCase()
}