首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

中國二代身份證號碼驗證算法深度解析與 Java 實現

     編輯:Java 程式設計入門

前言:身份證號碼不只是一串數字

中華人民共和國居民身份證號碼(18 位)遵循 GB 11643-1999 國家標準,編碼結構嚴謹、校驗算法科學。本文將從結構拆解、校驗原理、Java 實現、邊界情況、擴展應用五個維度徹底講透,並提供生產級工具類代碼。

1. 18 位身份證結構拆解

┌─────────────────────────────────────────────────────────────┐
│  1-6 地址碼  │  7-14 出生日期  │ 15-17 順序碼  │ 18 校驗碼  │
├──────────────┼─────────────────┼────────────────┼───────────┤
│  6 位數字    │  8 位數字        │  3 位數字       │ 1 位字符   │
│  GB/T 2260   │  GB/T 7408       │  奇男偶女       │ ISO 7064   │
└──────────────┴─────────────────┴────────────────┴───────────┘
段落位數含義規則/標準
地址碼1-6戶籍所在縣(市、區)行政區劃代碼GB/T 2260,需定期更新(如 2023 年行政區劃調整)
出生日期7-14YYYYMMDD 格式GB/T 7408,需校驗日期合法性(閏年、月份天數)
順序碼15-17同地址碼、同出生日期人員順序號奇數分配男性,偶數分配女性
校驗碼18ISO 7064:1983.MOD 11-2 算法值範圍 0-9 + X(代表 10)

2. 校驗碼算法:ISO 7064 MOD 11-2

步驟 1:加權求和

前 17 位數字分別乘以固定權重因子 Wi:


位置 i:   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17
權重 Wi:  7   9  10   5   8   4   2   1   6   3   7   9  10   5   8   4   2

S = Σ(Ai × Wi),其中 Ai 為第 i 位數字值(0-9)。

步驟 2:取模 11

Y = S mod 11,結果範圍 0-10。

步驟 3:查表得到校驗碼


Y:       0  1  2  3  4  5  6  7  8  9  10
校驗碼:  1  0  X  9  8  7  6  5  4  3   2

註:X 為大寫羅馬數字 10,僅出現在第 18 位。

3. Java 生產級實現


import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.regex.Pattern;

public final class IdCardValidator {

    private static final int[] WEIGHTS = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
    private static final char[] CHECK_CODES = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
    private static final Pattern ID18_PATTERN = Pattern.compile("^\\d{17}[\\dX]$");
    private static final Pattern ID15_PATTERN = Pattern.compile("^\\d{15}$");
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");

    private IdCardValidator() {}

    /**
     * 完整驗證:格式 + 日期合法 + 校驗碼
     *
     * @param idCard 身份證號碼(15 或 18 位)
     * @return 驗證結果對象
     */
    public static ValidationResult validate(String idCard) {
        if (idCard == null || idCard.isBlank()) {
            return ValidationResult.invalid("身份證號碼為空");
        }
        String id = idCard.trim().toUpperCase();

        // 15 位舊版(無校驗碼,年份兩位)
        if (ID15_PATTERN.matcher(id).matches()) {
            return validate15(id);
        }

        // 18 位新版
        if (!ID18_PATTERN.matcher(id).matches()) {
            return ValidationResult.invalid("格式錯誤:應為 15 位純數字或 18 位(數字+X)");
        }

        // 日期合法性
        String birthStr = id.substring(6, 14);
        LocalDate birthDate;
        try {
            birthDate = LocalDate.parse(birthStr, DATE_FMT);
        } catch (DateTimeParseException e) {
            return ValidationResult.invalid("出生日期非法: " + birthStr);
        }

        // 年齡合理性(可選:> 100 歲或 < 0 歲視為異常)
        int age = LocalDate.now().getYear() - birthDate.getYear();
        if (age < 0 || age > 120) {
            return ValidationResult.invalid("年齡異常: " + age + " 歲");
        }

        // 校驗碼
        char expected = calculateCheckCode(id.substring(0, 17));
        char actual = id.charAt(17);
        if (expected != actual) {
            return ValidationResult.invalid("校驗碼不匹配,期望: " + expected + ",實際: " + actual);
        }

        // 性別
        int genderCode = Character.getNumericValue(id.charAt(16));
        String gender = (genderCode % 2 == 1) ? "男" : "女";

        // 地址碼(可接入行政區劃庫進一步校驗)
        String addressCode = id.substring(0, 6);

        return ValidationResult.valid(id, birthDate, gender, addressCode);
    }

    /** 計算 17 位本體碼對應的校驗碼 */
    public static char calculateCheckCode(String id17) {
        if (id17 == null || id17.length() != 17) {
            throw new IllegalArgumentException("必須為 17 位數字");
        }
        int sum = 0;
        for (int i = 0; i < 17; i++) {
            sum += Character.getNumericValue(id17.charAt(i)) * WEIGHTS[i];
        }
        return CHECK_CODES[sum % 11];
    }

    /** 15 位轉 18 位(補全世紀、計算校驗碼) */
    public static String upgrade15to18(String id15) {
        if (!ID15_PATTERN.matcher(id15).matches()) {
            throw new IllegalArgumentException("非合法 15 位身份證");
        }
        String id17 = id15.substring(0, 6) + "19" + id15.substring(6); // 默認 19xx 年
        char check = calculateCheckCode(id17);
        return id17 + check;
    }

    private static ValidationResult validate15(String id15) {
        String birthStr = "19" + id15.substring(6, 12);
        try {
            LocalDate birth = LocalDate.parse(birthStr, DATE_FMT);
            int genderCode = Character.getNumericValue(id15.charAt(14));
            String gender = (genderCode % 2 == 1) ? "男" : "女";
            return ValidationResult.valid(id15, birth, gender, id15.substring(0, 6));
        } catch (DateTimeParseException e) {
            return ValidationResult.invalid("15 位出生日期非法");
        }
    }

    /** 驗證結果封裝 */
    public static class ValidationResult {
        private final boolean valid;
        private final String message;
        private final String idCard;
        private final LocalDate birthDate;
        private final String gender;
        private final String addressCode;

        private ValidationResult(boolean valid, String message, String idCard, LocalDate birthDate, String gender, String addressCode) {
            this.valid = valid;
            this.message = message;
            this.idCard = idCard;
            this.birthDate = birthDate;
            this.gender = gender;
            this.addressCode = addressCode;
        }

        public static ValidationResult valid(String idCard, LocalDate birthDate, String gender, String addressCode) {
            return new ValidationResult(true, "驗證通過", idCard, birthDate, gender, addressCode);
        }

        public static ValidationResult invalid(String message) {
            return new ValidationResult(false, message, null, null, null, null);
        }

        // Getters 略
        public boolean isValid() { return valid; }
        public String getMessage() { return message; }
        public String getIdCard() { return idCard; }
        public LocalDate getBirthDate() { return birthDate; }
        public String getGender() { return gender; }
        public String getAddressCode() { return addressCode; }

        @Override public String toString() {
            return valid ? "Valid: " + idCard + " (" + birthDate + ", " + gender + ")" : "Invalid: " + message;
        }
    }
}

4. 單元測試覆蓋關鍵邊界


import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class IdCardValidatorTest {

    @Test void testValid18() {
        // 真實測試數據(已脫敏)
        assertTrue(IdCardValidator.validate("11010119900307892X").isValid());
        assertTrue(IdCardValidator.validate("320123198506154321").isValid());
    }

    @Test void testInvalidCheckCode() {
        assertFalse(IdCardValidator.validate("110101199003078921").isValid()); // 最後一位錯
    }

    @Test void testInvalidDate() {
        assertFalse(IdCardValidator.validate("110101199002301234").isValid()); // 2 月 30 日
        assertFalse(IdCardValidator.validate("110101202501011234").isValid()); // 未來日期
    }

    @Test void testGender() {
        var r1 = IdCardValidator.validate("11010119900307892X"); // 第17位=9 奇數→男
        assertEquals("男", r1.getGender());
        var r2 = IdCardValidator.validate("110101199003078938"); // 第17位=3 奇數→男
        assertEquals("男", r2.getGender());
    }

    @Test void test15Upgrade() {
        String id18 = IdCardValidator.upgrade15to18("110101900307123");
        assertTrue(IdCardValidator.validate(id18).isValid());
    }

    @Test void testCalculateCheckCode() {
        assertEquals('X', IdCardValidator.calculateCheckCode("11010119900307892"));
        assertEquals('1', IdCardValidator.calculateCheckCode("32012319850615432"));
    }
}

5. 進階應用與避坑指南

地址碼實時校驗

行政區劃代碼隨政策變更(如 2020 年撤縣設區、2023 年部分鄉鎮合併)。生產系統建議:

  • 引入 area-code 庫(Maven Central 有現成項目)
  • 或維護一張 address_code (code, name, level, parent_code, valid_from, valid_to) 表,定期同步民政部公告
  • 校驗時檢查 birthDate 是否在 [valid_from, valid_to] 區間內

15/18 位共存場景

舊系統遷移時常遇到混存。建議統一轉換為 18 位存儲,查詢時按 18 位索引,顯示時按需脫敏(如 110101******892X)。

數據庫存儲優化

文中提到:「我們的一個項目,數據庫保存前 17 位數字,這樣對應一些 SQL 語句(比如 inner join)有加速作用」

原理:校驗碼非數字(含 X),導致字符串比較、索引效率略低。拆分存儲:


CREATE TABLE id_card (
    id_body CHAR(17) NOT NULL,      -- 前17位純數字,建立唯一索引
    check_code CHAR(1) NOT NULL,    -- 校驗碼
    full_id CHAR(18) GENERATED ALWAYS AS (CONCAT(id_body, check_code)) STORED,
    -- 其他字段...
    PRIMARY KEY (id_body)
);
  • JOIN 時直接 ON id_body,避免隱式轉換、字符集排序規則干擾
  • 校驗碼單獨列,便於統計分析(如 X 出現頻率)

脫敏展示最佳實踐


public static String mask(String idCard) {
    if (idCard == null || idCard.length() != 18) return "***";
    return idCard.substring(0, 6) + "********" + idCard.substring(14); // 保留地址碼+後4位
}
// 結果:110101********892X

總結

二代身份證驗證看似簡單,實則涉及編碼標準、日期曆法、模運算、行政區劃版本、性別編碼、數據庫優化、隱私保護等多維知識。本文提供的 IdCardValidator 涵蓋:

  • 格式正則預檢、日期合法性、校驗碼核算、性別解析
  • 15 位舊版兼容與自動升級
  • 結構化結果返回,便於業務層決策
  • 完整單元測試覆蓋邊界條件

直接引入項目即可投入生產,建議配合行政區劃代碼庫實現地址碼實時校驗,構建完整身份證合規體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved