Laravel API请求体中如何验证身份证18位格式及校验码正确性?
- 内容介绍
- 文章标签
- 相关推荐
本文共计880个文字,预计阅读时间需要4分钟。
身份证明字段的长度不能仅依靠长度判断,必须验证前17位数字与第18位校验码的组合逻辑。Laravel 自带的 digits:18 规则会漏掉字母X和校验逻辑,直接导致错误数据入库。
推荐用 Rule::regex() 写精准正则,覆盖:17位数字 + 末位数字或X(大小写都接受):
use Illuminate\Validation\Rule; <p>$rules = [ 'id_card' => [ 'required', 'string', Rule::regex('/^\d{17}[\dXx]$/'), ], ];
- 必须加
string类型约束,否则数字开头的ID会被自动转成int,触发类型不匹配失败 - 正则末尾用
[\dXx]而不是[0-9Xx],避免某些PHP版本下字符类解析差异 - 不要用
alpha_num,它允许纯数字但不校验位数,且放行非法字母
手写校验码算法验证必须放在自定义规则里
正则只能拦住明显格式错误,真正决定合法性的,是第18位校验码是否由前17位加权模11算出。这个逻辑没法用内置规则表达,必须封装成自定义验证规则。
在 App\Rules\ValidIdCard.php 中实现:
public function passes($attribute, $value) { if (!preg_match('/^\d{17}[\dXx]$/', $value)) { return false; } <pre class='brush:php;toolbar:false;'>$weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; $checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; $sum = 0; for ($i = 0; $i < 17; $i++) { $sum += (int)$value[$i] * $weights[$i]; } $mod = $sum % 11; return strtoupper($value[17]) === $checkCodes[$mod];
}
- 务必先过正则再算校验码,避免对非18位字符串做数组访问导致
Undefined offset - 用
strtoupper()统一处理X,不然小写x会校验失败 - 别在Controller里直接写这段逻辑——下次复用时还得抄一遍,也难测试
地区码和出生日期合法性需要额外检查
通过了位数+校验码,不代表就是真实身份证。比如 999999199901011234 格式全对,但地区码999999不存在,出生年份19990101也不合法。
建议在自定义规则中追加两步校验:
- 查地区码表:用
file_get_contents('https://raw.githubusercontent.com/elastic/elasticsearch/master/libs/geoip/src/main/resources/geolite2/country-codes.csv')不现实,应本地存一份精简版resources/data/idcard_areas.php,只含6位有效区划代码 - 解析出生日期:取
substr($value, 6, 8),用DateTime::createFromFormat('Ymd', $date)判断是否为真实日期(注意1900年前、未来日期、2月30日等边界) - 别用
checkdate()直接传年月日整数——它不校验字符串合法性,且对1900年前返回false,而老身份证确实存在1899年出生的情况
API请求体字段校验失败时别暴露细节
用户提交 id_card=123,如果返回 "The id card must match the regex...",等于告诉攻击者你用了正则;返回 "Invalid checksum" 更糟,直接泄露校验逻辑。
- 统一返回
"The id_card is invalid.",前端按字段名提示即可 - 调试阶段可在日志里记录具体失败原因,但绝不返回给客户端
- 如果项目启用了
APP_DEBUG=true,确保验证规则没抛出未捕获异常——否则会直接输出堆栈,暴露路径和类名
实际用起来,最易被跳过的其实是地区码和出生日期这两层。很多人测到校验码通过就以为万事大吉,结果上线后发现一堆“新疆生产建设兵团”开头的假号进来了。
本文共计880个文字,预计阅读时间需要4分钟。
身份证明字段的长度不能仅依靠长度判断,必须验证前17位数字与第18位校验码的组合逻辑。Laravel 自带的 digits:18 规则会漏掉字母X和校验逻辑,直接导致错误数据入库。
推荐用 Rule::regex() 写精准正则,覆盖:17位数字 + 末位数字或X(大小写都接受):
use Illuminate\Validation\Rule; <p>$rules = [ 'id_card' => [ 'required', 'string', Rule::regex('/^\d{17}[\dXx]$/'), ], ];
- 必须加
string类型约束,否则数字开头的ID会被自动转成int,触发类型不匹配失败 - 正则末尾用
[\dXx]而不是[0-9Xx],避免某些PHP版本下字符类解析差异 - 不要用
alpha_num,它允许纯数字但不校验位数,且放行非法字母
手写校验码算法验证必须放在自定义规则里
正则只能拦住明显格式错误,真正决定合法性的,是第18位校验码是否由前17位加权模11算出。这个逻辑没法用内置规则表达,必须封装成自定义验证规则。
在 App\Rules\ValidIdCard.php 中实现:
public function passes($attribute, $value) { if (!preg_match('/^\d{17}[\dXx]$/', $value)) { return false; } <pre class='brush:php;toolbar:false;'>$weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; $checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; $sum = 0; for ($i = 0; $i < 17; $i++) { $sum += (int)$value[$i] * $weights[$i]; } $mod = $sum % 11; return strtoupper($value[17]) === $checkCodes[$mod];
}
- 务必先过正则再算校验码,避免对非18位字符串做数组访问导致
Undefined offset - 用
strtoupper()统一处理X,不然小写x会校验失败 - 别在Controller里直接写这段逻辑——下次复用时还得抄一遍,也难测试
地区码和出生日期合法性需要额外检查
通过了位数+校验码,不代表就是真实身份证。比如 999999199901011234 格式全对,但地区码999999不存在,出生年份19990101也不合法。
建议在自定义规则中追加两步校验:
- 查地区码表:用
file_get_contents('https://raw.githubusercontent.com/elastic/elasticsearch/master/libs/geoip/src/main/resources/geolite2/country-codes.csv')不现实,应本地存一份精简版resources/data/idcard_areas.php,只含6位有效区划代码 - 解析出生日期:取
substr($value, 6, 8),用DateTime::createFromFormat('Ymd', $date)判断是否为真实日期(注意1900年前、未来日期、2月30日等边界) - 别用
checkdate()直接传年月日整数——它不校验字符串合法性,且对1900年前返回false,而老身份证确实存在1899年出生的情况
API请求体字段校验失败时别暴露细节
用户提交 id_card=123,如果返回 "The id card must match the regex...",等于告诉攻击者你用了正则;返回 "Invalid checksum" 更糟,直接泄露校验逻辑。
- 统一返回
"The id_card is invalid.",前端按字段名提示即可 - 调试阶段可在日志里记录具体失败原因,但绝不返回给客户端
- 如果项目启用了
APP_DEBUG=true,确保验证规则没抛出未捕获异常——否则会直接输出堆栈,暴露路径和类名
实际用起来,最易被跳过的其实是地区码和出生日期这两层。很多人测到校验码通过就以为万事大吉,结果上线后发现一堆“新疆生产建设兵团”开头的假号进来了。

