PHP校驗15位和18位身份證號

imjcw發表於2018-11-07

前言

看新公司框架原始碼的時候,發現了這個功能,於是搜尋一番並封裝了一下身份證號校驗的類。

目前大家的身份證號大多是 18 位的,當然,也不排除有些老人的身份證號是 15 位的。

如果強制要求是 18 位的話,會比較好,因為 15 位的身份證號沒有校驗碼,可以說,只要瞭解大概結構,隨手都可以造出一系列身份證號碼來。

當然,如果只是單純的程式校驗,18 位的身份證號碼也可以偽造,就是需要偽造者花點心思。

最好的還是呼叫相關部門給的介面,進行校驗。

本文所編寫的身份證號碼校驗,只是針對相關規則下的計算,是呼叫介面前能做的事情。

身份證號規則

15位: 省份(2位) + 地級市(2位) + 縣級市(2位) + 出生年(2位) + 出生月(2位) + 出生日(2位) + 順序號(3位)

18位: 省份(2位) + 地級市(2位) + 縣級市(2位) + 出生年(4位) + 出生月(2位) + 出生日(2位) + 順序號(3位) + 校驗位(1位)

相比之下,18位15位 多出生年 2位、校驗位 1位

其中,順序號如果是偶數,則說明是女生,順序號是奇數,則說明是男生。

校驗位的計算:

有17位數字,分別是:

7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2
複製程式碼

分別用身份證的前 17 位乘以上面相應位置的數字,然後相加。

接著用相加的和對 11 取模。

用獲得的值在下面 11 個字元裡查詢對應位置的字元,這個字元就是校驗位。

'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'
複製程式碼

15位轉18位:

從上述的分析中,可以知道,只要補充上年分和校驗位就可以了。

一般情況下年份補充都是加上 19 就可以了。

校驗類的實現

通過分析身份證號的規則,瞭解到,有幾點是可以做的:

  • 檢查身份是否正確(一般不會變化,而且省份不多)
  • 檢查地級市和縣級市(如果有這方面的資源,可以考慮,不過一般不建議)
  • 檢查年月日
  • 檢查校驗碼

當然,因為可能部分人用的是 15位 的身份證號,所以需要一個轉換的方法,不過,這裡還是建議限制需要 18位 的身份證號。

下面開始實現:

初始化:

class IDCardFilter
{
    /**
     * 身份證號碼校驗
     *
     * @param  string $idCard
     * @return bool
     */
    public function vaild($idCard)
    {
        // 基礎的校驗,校驗身份證格式是否正確
        if (!$this->isCardNumber($idCard)) {
            return false;
        }

        // 將 15 位轉換成 18 位
        $idCard = $this->fifteen2Eighteen($idCard);

        // 檢查省是否存在
        if (!$this->checkProvince($idCard)) {
            return false;
        }

        // 檢查生日是否正確
        if (!$this->checkBirthday($idCard)) {
            return false;
        }

        // 檢查校驗碼
        return $this->checkCode($idCard);
    }
}
複製程式碼

上面已經實現了一個校驗的方法,裡面呼叫了類裡的很多方法,下面一一實現。

檢測是否是身份證號碼:

這一塊的處理比較簡單,一個正規表示式搞定了。

其中,(^\d{15}$) 用於匹配 15位 身份證號的情況;(^\d{17}(\d|X)$) 用於匹配 18位 身份證號的情況。

const REGX = '#(^\d{15}$)|(^\d{17}(\d|X)$)#';

/**
 * 檢測是否是身份證號碼
 *
 * @param  string $idCard
 * @return boolean
 */
public function isCardNumber($idCard)
{
    return preg_match(self::REGX, $idCard);
}
複製程式碼

15位轉18位:

邏輯不復雜,先判斷是否是15位,然後判斷需要新增的年份,最終生成校驗碼拼接返回就OK了。

/**
 * 15位轉18位
 *
 * @param  string $idCard
 * @return void
 */
public function fifteen2Eighteen($idCard)
{
    if (strlen($idCard) != 15) {
        return $idCard;
    }

    // 如果身份證順序碼是996 997 998 999,這些是為百歲以上老人的特殊編碼
    // $code = array_search(substr($idCard, 12, 3), [996, 997, 998, 999]) !== false ? '18' : '19';
    // 一般 19 就夠了
    $code = '19';
    $idCardBase = substr($idCard, 0, 6) . $code . substr($idCard, 6, 9);
    return $idCardBase . $this->genCode($idCardBase);
}
複製程式碼

校驗碼的生成:

詳細計算規則見上面,這裡就不做重複的闡述了。

/**
 * 生成校驗碼
 *
 * @param  string $idCardBase
 * @return void
 */
final protected function genCode($idCardBase)
{
    $idCardLength = strlen($idCardBase);
    if ($idCardLength != 17) {
        return false;
    }
    $factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
    $verifyNumbers = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
    $sum = 0;
    for ($i = 0; $i < $idCardLength; $i++) {
        $sum += substr($idCardBase, $i, 1) * $factor[$i];
    }
    $index = $sum % 11;
    return $verifyNumbers[$index];
}
複製程式碼

檢查省份是否正確:

protected $provinces = [
    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 => "澳門", 91 => "國外"
];

/**
 * 檢查省份是否正確
 *
 * @param  string $idCard
 * @return void
 */
public function checkProvince($idCard)
{
    $provinceNumber = substr($idCard, 0, 2);
    return isset($this->provinces[$provinceNumber]);
}
複製程式碼

檢測生日是否正確:

這裡也是用正則匹配,匹配出年月日的。

/**
 * 檢測生日是否正確
 *
 * @param  string $idCard
 * @return void
 */
public function checkBirthday($idCard)
{
    $regx = '#^\d{6}(\d{4})(\d{2})(\d{2})\d{3}[0-9X]$#';
    if (!preg_match($regx, $idCard, $matches)) {
        return false;
    }
    array_shift($matches);
    list($year, $month, $day) = $matches;
    return checkdate($month, $day, $year);
}
複製程式碼

校驗碼比對:

話說,15位18位 的都完全不用考慮這個方法了。

/**
 * 校驗碼比對
 *
 * @param  string $idCard
 * @return void
 */
public function checkCode($idCard)
{
    $idCardBase = substr($idCard, 0, 17);
    $code = $this->genCode($idCardBase);
    return $idCard == ($idCardBase . $code);
}
複製程式碼

完整程式碼

傳送門:IDCardFilter

最後

這個功能最多算是新穎吧,畢竟之前沒有接觸過。很開心程式碼片段裡又增加了新的成員。

-- EOF -- 本文轉載自IMJCW 原文連結:PHP校驗15位和18位身份證號

相關文章