身份证校验

一、身份证规则

根据〖中华人民共和国国家标准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

验证
image

三、代码

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
image

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

posted @ 2021-10-13 00:06  ohmok  阅读(1080)  评论(0)    收藏  举报