身份证校验
一、身份证规则
根据〖中华人民共和国国家标准GB11643-1999〗中有关公民身份号码的规定,公民身份号码是特征组合码,由十七位数字本体码和一位数字校验码组成。
排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。
身份证号编码规则
| 位数 | 含义 |
|---|---|
| 1~2 | 所在省(直辖市、自治区)的代码 |
| 3~4 | 所在地级市(自治州)的代码 |
| 5~6 | 所在区(县、自治县、县级市)的代码 |
| 7~10 | 出生年份 |
| 11~12 | 出生月份 |
| 13~14 | 出生日 |
| 15~16 | 所在地的派出所代码 |
| 17 | 奇数代表男性,偶数代表女性 |
| 18 | 校验码,生成规则可参考 ISO 7064:1983.MOD 11-2 算法 |
二、校验规则介绍
1)正则校验
规则如下:
- 第一位不可能是0
- 第二位到第六位可以是0-9
- 第七位到第十位是年份,所以七八位为19或者20
- 十一位和十二位是月份,这两位是01-12之间的数值
- 十三位和十四位是日期,是从01-31之间的数值
- 十五,十六,十七都是数字0-9
- 十八位可能是数字0-9,也可能是X
正则表达式为:
private static final String CHINA_ID_REGEX = "([1-9]\\d{5})" +
"((19|20)\\d{2}(0[1-9]|1[0-2])((0[1-9])|((1|2)[0-9])|30|31))" +
"(\\d{3})" +
"(\\d|X|x)$";
缺点: 正则校验只是基本的格式判定,存在三个主要的不足
- 地址码判定不够精确。例:我国并不存在16,26开头的地区,却可通过验证
- 日期判定不够精确。例:20990231也可通过验证,而2099年还要很久并且2月不存在31日
- 校验码是由17位本体码计算得出,正则并未校验此码
2)算法校验 ISO 7064:1983.MOD 11-2
以 441226202202023458 为例
校验流程
① 加权因子求和,
加权因子表: [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
4*7 + 4*9 + 1*10 + 2*5 + 2*8 + 6*4 +
2*2 + 0*1 + 2*6 + 2*3 + 0*7 + 2*9 + 0*10 + 2*5 +
3*8 + 4*4 + 5*2
取得身份证号前17位与对应的加权因子值相乘的和为 224
② 对11取余
校验码表: ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]
将和值与11取模得到余数作为索引对校验码表匹配
224 % 11 = 4
取余结果为 4 对应校验码表里的 8
因此身份证最后一位校验码为 8
③ 验证

三、代码
public class ValidateIdCardUtil {
/**
* 中国公民身份证号码最大长度。
*/
public static final int CHINA_ID_MAX_LENGTH = 18;
/**
* 18为身份证正则
* <p>
* 1-6位为地址码【 1-2:省,不能0开头 | 3-4:市 | 5-6:县或区 】
* 7-15位为日期码【 1-4:年,取值范围'19xx或20xx' | 5-6:月,取值范围'01-12' | 7-8:日,取值范围'01-31' 】
* 15-17为顺序码【 1-2:派出所编码 | 3:性别 】
* 18位为校验码,可以是数字或大小写的X
* </p>
*/
private static final String CHINA_ID_REGEX = "([1-9]\\d{5})" +
"((19|20)\\d{2}(0[1-9]|1[0-2])((0[1-9])|((1|2)[0-9])|30|31))" +
"(\\d{3})" +
"(\\d|X|x)$";
/**
* 内地省、直辖市代码表
*/
public static Map<String, String> cityCodes = new HashMap<>();
/**
* 每位加权因子
*/
private static final int[] POWER = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
/**
* 第十八位校验码
*/
private static final String[] VERIFY_CODE = {"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"};
static {
cityCodes.put("11", "北京");
cityCodes.put("12", "天津");
cityCodes.put("13", "河北");
cityCodes.put("14", "山西");
cityCodes.put("15", "内蒙古");
cityCodes.put("21", "辽宁");
cityCodes.put("22", "吉林");
cityCodes.put("23", "黑龙江");
cityCodes.put("31", "上海");
cityCodes.put("32", "江苏");
cityCodes.put("33", "浙江");
cityCodes.put("34", "安徽");
cityCodes.put("35", "福建");
cityCodes.put("36", "江西");
cityCodes.put("37", "山东");
cityCodes.put("41", "河南");
cityCodes.put("42", "湖北");
cityCodes.put("43", "湖南");
cityCodes.put("44", "广东");
cityCodes.put("45", "广西");
cityCodes.put("46", "海南");
cityCodes.put("50", "重庆");
cityCodes.put("51", "四川");
cityCodes.put("52", "贵州");
cityCodes.put("53", "云南");
cityCodes.put("54", "西藏");
cityCodes.put("61", "陕西");
cityCodes.put("62", "甘肃");
cityCodes.put("63", "青海");
cityCodes.put("64", "宁夏");
cityCodes.put("65", "新疆");
cityCodes.put("71", "台湾");
cityCodes.put("81", "香港");
cityCodes.put("82", "澳门");
cityCodes.put("91", "国外");
}
/**
* 判断18位身份证是否合法
*
* @param idCard
* @return
*/
private boolean isValidate18IdCard(String idCard) {
// 非18位为假,正则失败为假
if (idCard.length() != CHINA_ID_MAX_LENGTH && !idCard.matches(CHINA_ID_REGEX)) {
return false;
}
// 获取前17位
String idCard17 = idCard.substring(0, 17);
char[] c = null;
// 获取第18位
String idCard18Code = idCard.substring(17, 18);
String checkCode = "";
// 验证idCard17是否都为数字
if (isDigital(idCard17)) {
// 校验地址码,验证省、直辖市代码。市、区不作验证
boolean checkCityCode = cityCodes.containsKey(idCard.substring(0, 2));
// 校验出生日期码
boolean checkDateCode = checkDateCode(idCard);
if (!checkCityCode || !checkDateCode) {
return false;
}
c = idCard17.toCharArray();
} else {
return false;
}
if (null != c) {
int[] bit = new int[idCard17.length()];
// 将字符数组转为整型数组
bit = converCharToInt(c);
// 获取前17位乘加载因子之和
int sum17 = getPowerSum(bit);
// 将和值与11取模得到余数进行校验码判断
checkCode = getCheckCodeBySum(sum17);
if (null == checkCode) {
return false;
}
// 将身份证的第18位与算出来的校码进行匹配,不相等就为假
if (!idCard18Code.equalsIgnoreCase(checkCode)) {
return false;
}
}
return true;
}
/**
* 数字验证
*
* @param str
* @return
*/
private boolean isDigital(String str) {
return str == null || "".equals(str) ? false : str.matches("^[0-9]*$");
}
/**
* 校验出生日期码
*/
private boolean checkDateCode(String idCard) {
// 获取身份证日期码
String dateCode = idCard.substring(6, 14);
// 如果输入的日期为20180231,通过转换的后realBirthday为20180303
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, Integer.parseInt(idCard.substring(6, 10)));
calendar.set(Calendar.MONTH, Integer.parseInt(idCard.substring(10, 12)) - 1);
calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(idCard.substring(12, 14)));
String birthday = new SimpleDateFormat("yyyyMMdd").format(calendar.getTime());
// 该身份证生出日期在当前日期之后时为假
if (calendar.getTimeInMillis() >= System.currentTimeMillis()) {
return false;
}
// 比较日期是否相等
return dateCode.equals(birthday);
}
/**
* 将字符数组转为整型数组
*
* @param c
* @return
* @throws NumberFormatException
*/
private int[] converCharToInt(char[] c) throws NumberFormatException {
int[] arr = new int[c.length];
int k = 0;
for (char temp : c) {
arr[k++] = Integer.parseInt(String.valueOf(temp));
}
return arr;
}
/**
* 取得身份证号前17位与对应的权重值相乘的和
*
* @param bit
* @return
*/
private int getPowerSum(int[] bit) {
int sum = 0;
if (POWER.length != bit.length) {
return sum;
}
for (int i = 0; i < bit.length && POWER.length == bit.length; i++) {
sum = sum + bit[i] * POWER[i];
}
return sum;
}
/**
* 将和值与11取模得到余数进行校验码判断
*
* @param sum17
* @return 校验位
*/
private String getCheckCodeBySum(int sum17) {
return VERIFY_CODE[sum17 % 11];
}
}
此校验方式也存在漏洞
- 地址码这里只校验了省,地市和县区没有做验证
- 顺序码这里也可以作假随便填,毕竟没有详细的数据可做校验
只要前面17位通过简单的校验然后再获取对应的校验码都可以逃过校验!!!如:110109209912316661

目前应该除了公安系统的API应该也没有较为准确的验证方法。
以上为个人见解,如有问题可留言交流。

浙公网安备 33010602011771号