做后端开发这些年接到“港澳台身份证验证”需求不是一次两次了。需求方通常给的描述很简单“在注册/下单/提现页面校验一下港澳台证件号格式就行”。但真正开工就会发现这玩意儿不是写两行正则能搞定的——台湾地区身份证有严格的校验码算法香港地区身份证的括号校验位规则各家实现还不一样澳门地区更有新旧两套格式并存的问题。如果照搬网上搜来的代码大概率会在真实用户提交时翻车。这篇文章我会把这套验证逻辑从规则到代码完整拆开给你一个可以直接抄进 PHP 项目里的验证类同时把我在实际接单中踩过的坑一并交代清楚。适合做电商、会员系统、旅游平台、金融风控后端的 PHP 开发者参考哪怕你只是临时要给表单加个校验也能从这里找到现成方案。1. 先梳理需求港澳台身份证验证到底要解决什么问题1.1 三个地区的证件编号差异远超想象先说结论台湾、香港、澳门三个地区的身份证号码从位数、字符组成到校验逻辑几乎没有任何共同点。把它们扔进同一个“身份证格式校验”函数里硬要用一套正则通杀只会得到一个形同虚设的结果。地区位数字符组成校验能力台湾地区10 位1 个英文字母 9 个数字有明确校验码算法可精确校验香港地区主体 8~9 位1~2 个英文字母 6~7 个数字括号内还有校验字符官方算法未完整公开常用 MOD 11 思路模拟澳门地区8 位为主现行样式多为 8 位数字旧式可能含 1~2 个字母无权威公开校验规则只能做格式自洽权重校验台湾地区的识别最容易理解它的编号算法严格算出来的校验位一旦对不上这串号码基本就是错的。香港地区最大的迷惑点是那个括号比如A234567(4)括号里的字符也是身份证的一部分很多人做正则时会漏掉。澳门地区则是新旧两代证件并存前端老用户手里还可能是带字母的旧卡如果只允许纯数字老用户会被直接卡死。1.2 本地校验与官方核验的边界这是整个需求中最重要、也最容易被业务方误解的一点。本地校验函数能做的只是“格式防呆”。它根据公开的字母映射、加权求和、取模规则把明显不全、抄错、乱编的身份证号拦在门外。台湾地区能做到比较高的准确率因为它算法公开香港和澳门地区没有完整公开校验规则本地代码只能算自洽模拟不能拍胸脯说“按我这段代码校验通过就一定是真证”。真正的“实名核验”比如确认这个证件号跟持证人是同一人、证件没有注销、没有被列入异常名单必须走当地主管部门的官方接口或者接入已获得授权资质的三方实名核验服务。在我接过的项目里凡是涉及资金提现、开户、高风险操作的系统我都会在文档里跟产品经理讲清楚这条边界本地校验用于用户体验层面的即时拦截官方校验用于最终的实名链路。2. 三个地区的证件格式与校验规则拆解2.1 台湾地区身份证字母映射加乘权取余台湾地区身份证号码的官方名称叫“统一编号”结构是这样的第一位是英文字母代表初次登记的地域第二位是性别码数字 1 代表男性、2 代表女性第三到第九位是流水号第十位是校验码。整个人就是A123456789这样的形态。校验算法分四步第一步把首字母查表换成两位数字。A对应 10B对应 11逐个往后排Z对应 35。查表值10~35必须是两位数这为后面的加权计算做了铺垫。第二步将查表得到的两位数拆开十位数乘以 1个位数乘以 9两者相加。比如A10就是1*1 0*9 1。这一步相当于把字母区域信息嵌进校验体系。第三步把身份证第二位到第九位也就是性别码加七位流水分别乘上权重8,7,6,5,4,3,2,1再加到第二步的结果里。第四步对总和取模 10然后用10 - 余数得到校验位。如果校验位刚好等于 10就按 0 处理。拿演示号码A123456789算一遍查表A10第二步得到 1第三到第九位的数字是12345678加权求和是1*8 2*7 3*6 4*5 5*4 6*3 7*2 8*1 120总和1 120 121取模 10 得到 1校验位是10 - 1 9。这与A123456789的最后一位吻合。这里有个细节值得注意性别码强制要求1或2这是台湾本地居民的规则。后来外籍人士在台湾地区取得统一证号时第二位出现了8、9等其他取值。如果你的业务面向长期居留的外籍人士正则里要把[12]放开成\d可不要写死了。2.2 香港地区身份证主体加括号校验位香港地区身份证号码常见的显示方式是A234567(4)、AB987654(3)这类。主体部分由 1~2 个大写字母加 6~7 位数字构成括号内还有一个校验字符可能是数字也可能是字母A。它的校验思路也走“加权求和取模”这一路风格。先把主体部分的每个字符合成一个序列字母按字母表顺序转成数字A1、B2以此类推数字保持原值。然后给序列配权重权重从主体长度递减到 1。比如主体A234567有 7 个字符权重就是7,6,5,4,3,2,1。加权求和后对 11 取模再用(11 - 余数) % 11得到期望校验值。如果期望值是 10就用字母A作为括号里的校验字符否则就是数字本身。用演示号码A234567(4)验证字母A转换后是 1加权和是1*7 2*6 3*5 4*4 5*3 6*2 7*1 84。84 对 11 取模得到 7(11 - 7) % 11 4括号校验位确实是4。我要特别重申一个事实香港地区政府没有对外公布完整的官方校验算法文档网上流传的 PHP、Java、Python 版实现基本都是从业者根据样例反推或自行模拟的。我下面给出的这套是“能自洽、能防呆”的 MOD 11 版本适合在注册、报名、留资这些风险等级不高的场景做格式过滤但不要把它当成官方核验标准。2.3 澳门地区身份证新旧并存规则不透明澳门地区身份证最让人头疼因为新旧两代证件的编码风格差别很大。旧式身份证可能出现“一个字母 6~7 位数字”的结构比如M1234567。新式智能身份证则普遍是 8 位纯数字。字符组成变了校验逻辑又没有公开导致很多团队在这里选择“只做长度和字符集校验”。我处理的方案是先用宽松正则兼容两种主流格式然后对新式 8 位纯数字采用一个自洽的加权取模 10 算法做二次拦截。具体的权重是给前 7 位数字分配7,6,5,4,3,2,1加权求和后取模 10再用(10 - 余数) % 10得到第 8 位校验数字。拿演示号码12345676算一遍前 7 位是1234567加权和1*7 2*6 3*5 4*4 5*3 6*2 7*1 84取模 10 得到 4校验位是10 - 4 6最后一位正好是 6。同样这套自洽校验不代表澳门官方认证它只是比“只有正则”更严一层。如果业务对澳门证件准确率要求很高要么人工审核证件照片要么走具备资质的三方核验通道。3. PHP 实现一个验证类搞定三个地区3.1 统一入口和输入清洗我建议把三个地区的校验逻辑封装到一个静态类里对外只暴露一个validate($idCard, $region)方法。调用方不用关心内部差异传入证件号和地区代码即可。进入正式校验前输入清洗是必须做的一步。真实表单里用户会输入各种意外内容小写字母、前后空格、全角括号、中文括号、甚至把(4)写成4。如果不先做标准化再好的正则也会漏判。mb_convert_kana可以把全角字符统一转成半角这个函数依赖mbstring扩展大部分 PHP 环境都带。没有这个扩展时至少要处理空格和大小写。public static function validate(string $idCard, string $region): bool { $idCard self::normalize($idCard); switch (strtoupper($region)) { case TW: return self::validateTaiwan($idCard); case HK: return self::validateHongKong($idCard); case MO: return self::validateMacau($idCard); } throw new InvalidArgumentException(Unsupported region: . $region); } private static function normalize(string $idCard): string { $idCard trim($idCard); if (function_exists(mb_convert_kana)) { $idCard mb_convert_kana($idCard, rnas, UTF-8); } return strtoupper($idCard); }3.2 台湾地区校验代码实现台湾地区校验的核心是字母映射表和 8 位加权循环。注意intdiv是 PHP 7 才有的函数老项目如果跑在 PHP 5.6要换成(int)($mapValue / 10)。private static function validateTaiwan(string $id): bool { if (!preg_match(/^[A-Z][12][0-9]{8}$/, $id)) { return false; } $letterMap [ A10, B11, C12, D13, E14, F15, G16, H17, I18, J19, K20, L21, M22, N23, O24, P25, Q26, R27, S28, T29, U30, V31, W32, X33, Y34, Z35, ]; $mapValue $letterMap[$id[0]]; $sum intdiv($mapValue, 10) ($mapValue % 10) * 9; $weights [8, 7, 6, 5, 4, 3, 2, 1]; for ($i 0; $i 8; $i) { $sum (int)$id[$i 1] * $weights[$i]; } $check (10 - ($sum % 10)) % 10; return $check (int)$id[9]; }这段代码里的正则我写了[12]作为第二位性别码约束。如果你的业务需要兼容外籍人士统一证号把[12]改成\d即可后面的加权逻辑不用动。3.3 香港地区校验代码实现香港地区的难点在于解析括号校验位。我封装了一个parseBracketedId方法把主体和括号里的校验字符分开。主体可以是 1~2 个字母加 6~7 位数字不带括号时只做格式校验带括号时严格核对校验字符。private static function validateHongKong(string $id): bool { $parsed self::parseBracketedId($id); if ($parsed null) { return false; } [$body, $check] $parsed; $chars str_split($body); $n count($chars); $sum 0; foreach ($chars as $i $ch) { $value ctype_digit($ch) ? (int)$ch : ord($ch) - 64; $sum $value * ($n - $i); } $expected (11 - ($sum % 11)) % 11; $expectedChar $expected 10 ? A : (string)$expected; if ($check ) { return true; } return $expectedChar $check; } private static function parseBracketedId(string $id): ?array { if (preg_match(/^([A-Z]{1,2}[0-9]{6,7})$/, $id, $matches)) { return [$matches[1], ]; } if (preg_match(/^([A-Z]{1,2}[0-9]{6,7})\(([0-9A])\)$/, $id, $matches)) { return [$matches[1], $matches[2]]; } return null; }parseBracketedId里第一个正则匹配不带括号的主体第二个正则匹配完整的“主体(校验位)”形态。两个分支都要保留因为不同业务场景里用户输入习惯不一样有人会照抄证件卡面连括号一起填有人只会填一排数字字母。3.4 澳门地区校验代码实现澳门地区的兼容策略是8 位纯数字走加权取模 10 校验带字母的旧式格式只做格式校验。这符合“新式严格旧式宽松”的处理原则。private static function validateMacau(string $id): bool { if (preg_match(/^[0-9]{8}$/, $id)) { $sum 0; for ($i 0; $i 7; $i) { $sum (int)$id[$i] * (7 - $i); } $expected (10 - ($sum % 10)) % 10; return $expected (int)$id[7]; } if (preg_match(/^[A-Z]{1,2}[0-9]{6,7}$/, $id)) { return true; } return false; }这套澳门逻辑有一个天然限制它只能识别“纯 8 位数字”和“1~2 个字母 6~7 位数字”两种格式。如果未来澳门地区再更新证件编码规则需要第一时间跟进调整正则。我在项目里会把所有地区校验规则抽成配置方便这类变更时只改一处。4. 在常见框架中接入验证器4.1 Laravel 自定义验证规则Laravel 是 PHP 社区最常用的框架之一要在里面接入验证器最直接的办法是注册一个自定义规则。Laravel 9 及以后版本里推荐实现Illuminate\Contracts\Validation\Rule接口旧版可以直接用Validator::extend。我习惯把地区代码放进验证规则的构造函数里让同一个规则类可以复用到三个地区use Illuminate\Contracts\Validation\Rule; class TaiGangAoIdRule implements Rule { private string $region; public function __construct(string $region) { $this-region $region; } public function passes($attribute, $value): bool { return TaiGangAoIdValidator::validate($value, $this-region); } public function message(): string { return 输入的证件号码格式不正确; } }控制器里再配合已有的表单请求验证用法很自然$request-validate([ tw_id_card [required, new TaiGangAoIdRule(TW)], ]);这样既不会污染框架自带的验证规则列表也方便按地区分别定义错误提示。4.2 ThinkPHP 验证器扩展ThinkPHP 使用自定义验证规则时可以在验证器类里定义一个checkTaiGangAoId方法框架会自动把它当成规则调用。返回true表示通过返回字符串表示错误提示。class UserValidate extends Validate { protected $rule [ tw_id_card checkTaiGangAoId:TW, ]; protected function checkTaiGangAoId($value, $region): bool { return TaiGangAoIdValidator::validate($value, $region); } }这段示例让规则本身保持轻量真正的计算逻辑依然在独立验证类里避免验证器类越来越臃肿。4.3 返回结构化错误信息有些场景下前端想区分“格式错误”“校验码错误”“地区不支持”几类情况。这时把布尔值改成结构化返回会更友好。我会额外写一个方法public static function check(string $idCard, string $region): array { try { $valid self::validate($idCard, $region); } catch (\InvalidArgumentException $e) { return [valid false, error $e-getMessage()]; } return [ valid $valid, error $valid ? : 格式或校验码不正确, ]; }这个方法适合作为接口的入参校验层前端收到validfalse就直接展示error字段不用自己维护一套错误码映射。5. 实测用例与避坑清单5.1 每个地区能通过和不能通过的测试用例我把内部测试时常用的几个演示号码列出来方便你写完代码后快速自测。这些号码都是根据算法反推出来的没有对应真实持证人。地区输入号码预期结果说明台湾A123456789通过加权校验位正确台湾A123456788不通过末尾校验码错一位香港A234567(4)通过主体加权取模 11 结果正确香港A234567(5)不通过括号校验码不匹配香港A234567通过未提供括号校验码时只查格式澳门12345676通过8 位纯数字加权取模 10 正确澳门12345677不通过末位校验码不匹配澳门M1234567通过旧式字母开头仅格式校验这组用例跑完后三个分支的覆盖率基本就到位了。真正上线前建议再用真实表单里出现的脏数据做一轮补充测试重点看全角括号、空格、小写字母能不能被清洗逻辑吞掉。5.2 最容易踩的五个坑第一个坑是全角括号。用户从手机输入法里敲出来的经常是4而不是(4)没做全角转半角正则直接就不匹配。mb_convert_kana的效果是最省心的若没有扩展至少要把全角括号、全角数字、全角字母各自替换成半角。第二个坑是小写字母。很多人输入证件号时习惯顺手把所有英文字母打小写虽然身份证卡面是英文大写但业务侧没必要跟这个较劲在normalize里统一strtoupper即可。第三个坑是台湾地区性别码。早期统一编号的第二位确实只有1和2但如果业务会碰到外籍配偶或长期居留人士那里可能是8、9。正则写太死就会误伤真实用户。第四个坑是香港地区的旧式和新式宽度。旧式身份证可能是单字母加 6 位数字新式智能身份证可能出现双字母加 6 位或 7 位数字。正则里用{1,2}和{6,7}就是为了同时兼容这两种宽度别为了“看起来严谨”而写死成 8 位。第五个坑是把本地校验结果当成实名核验结果。无论代码里实现了多少个地区算法都改变不了“证件真伪由官方数据决定”这个事实。线上业务必须在文档和对接方协议里写清楚本地校验只负责过滤明显错误和改善用户体验高风险操作必须走官方核验通道。6. 实操中的经验和心得6.1 把校验逻辑抽成纯静态类的好处我最初几个项目里台湾、香港、澳门三套校验代码直接写在控制器里后来发现维护成本很高。比如香港地区算法从网上抄了一版不完善的测试用例都没跑就上线线上报错后才去改。现在我把整套逻辑收进一个纯静态类控制器、命令行任务、消息队列处理、单元测试都能直接调用不依赖任何全局状态。需要调整规则时改一个文件即可不会出现“控制器 A 改好了、控制器 B 还留着旧逻辑”的问题。6.2 校验失败时错误信息要具体很多开发者偷懒失败统一返回“证件号码格式不正确”。这种提示对用户极不友好——到底哪里不正确是长度不够还是校验码错了还是用了不被支持的地区我的建议是至少区分三层正则不匹配时提示“长度或字符组成不符合规则”加权校验失败时提示“校验码不匹配请核对”地区参数非法时提示“不支持该地区代码”。前端拿到具体错误用户才能更快纠正。6.3 后续如果需要接入官方核验怎么改这套本地验证类天然适合作为官方核验的前置层。接入官方接口或三方实名核验服务时可以把validate()放在第一步拦截掉低质量号码减少对高成本核验接口的无效调用。官方核验返回后再用结果覆盖掉本地结论即可。我在项目里就是这么设计的本地校验意味着“格式可达标”官方核验才意味着“真实有效”。两者各司其职比单纯依赖其中任何一边都稳妥。最后再分享一个实际操作中的小技巧自洽校验算法这类代码写完一定要留一张测试用例表在代码注释里。六个月后你回来维护或者换个同事接手对照用例表能一眼看出每个地区断言是不是符合预期省去重新推导算法的功夫。
阅读完成 · 觉得有帮助?