资讯动态

安卓NFC读身份证:协议边界与SAM授权真相

发布时间:2026/10/14 18:59:28 来源:尧图企业网站定制
简介面向Android开发者的NFC读身份证项目源码包围绕二代证芯片读取与云解码展开讲解NFC工作原理、ISO/IEC 14443标准、身份证信息采集及通过HTTP将数据交由云端解析的完整链路适合有Android基础并希望实现证件识别、酒店入住、机场安检等场景的开发学习者。资源共458个文件压缩包约15.41MB包含Java源码、Gradle构建脚本、XML布局配置、JAR/AAR依赖以及大量CMake、ninja缓存等工程文件适合作为可直接导入Android Studio的完整项目结构参考。整体结构便于对照源码逐模块阅读。目前已有3609人学习下载适合需要参考NFC读写、云解码、网络请求与工程配置整合思路的开发者可对照关键类梳理实现方式并对身份证敏感信息的传输与授权处理形成更完整认知节省从零搭建和排查问题的时间。1. 先说结论安卓 NFC 读身份证答案不在“读取”而在“授权”如果有人让你“找个安卓 NFC 读身份证的源码”你第一反应千万别是去 GitHub 搜而是先反问一句你要的是“读这张卡”还是“读这张卡里的姓名和证件号”这两个需求隔着一条鸿沟。先说结论普通手机的 NFC 天线能发现身份证这张卡能建立物理连接能拿到 Type B 协议层的 ATQB、PUPI 这类标识但拿不到卡里的明文信息。原因不是手机硬件不够而是身份证应用层有一套密钥认证机制密钥只存在于发卡机构认可的硬件安全模块里不会出现在任何一台手机的系统镜像中。市面上让你觉得“手机扫一下就出身份证信息”的代码要么是配了外置读卡器要么是拿假数据给你画饼。这篇文章我会把协议边界、能复现的最小代码、以及真正可交付的产品架构一次讲清楚适合做门禁、访客登记、自助终端、实名认证的开发者读。2. 身份证的 NFC 底层Type B 协议和普通手机能读到的那一层2.1 为什么不是“扫一下就出来”身份证卡片的协议结构大多数人理解的 NFC 读卡是拿手机贴一下园区门禁卡或者公交卡App 里立刻弹出一串卡号。这类卡走的是 ISO 14443 Type A 协议手机 NFC 芯片里有完整的 Type A 命令集所以卡号几乎是无条件可读的。身份证不一样它走的是 ISO 14443 Type B物理层调制方式和 Type A 就不一样Type A 用 100% ASK 调制Type B 用 10% ASK 调制Type B 的帧格式还多了 SOF、EOF 和 BCH 校验抗干扰设计完全不同。好在现在的手机 NFC 芯片基本都同时支持 Type A、Type B、Type F所以硬件层面不是障碍。真正的障碍在应用层。身份证卡内存放的不是一个可以被随便读出来的文本文件而是一组受访问控制的数据对象。要读取这些对象卡片和读卡器之间需要完成挑战-响应认证认证凭据是一套不会导出硬件之外的安全密钥。换句话说就算你有办法把整张卡的二进制数据块全部读出来拿到的也是密文没有密钥就是一堆无法解析的字节。很多人会把“手机能感应到身份证”误当成“手机能读身份证”。感应到只代表物理层握手成功和拿到业务数据是两码事。这个认知差是后面所有翻车现场的共同根源。2.2 普通安卓能读到什么ATQB、PUPI 与应用数据Android 的 NFC 框架里与身份证直接相关的是NfcB这个技术类它对应 ISO 14443-3B 协议层。当一张 Type B 卡片进入手机射频场手机会先发送 REQB 命令卡片回复 ATQB。ATQB 里包含 PUPIPseudo-Unique PUPI Identifier、应用数据、协议信息等字段。在 Android 里Tag.getId()拿到的就是这张卡唤醒时返回的 PUPINfcB.getApplicationData()拿到的是 ATQB 中的应用数据部分。这些数据能证明什么只能证明“这是一张 Type B 卡片且它的应用数据长这样”。它证明不了这张卡是身份证因为银行卡、部分电子证件、某些门禁卡也都是 Type B。这也是协议层的局限物理层设计出来是通用的不携带发卡机构认知。很多草台班子拿一个 NfcB 的 Demo 就宣称“已跑通身份证读取”其实只是把 ATQB 打了出来离真实业务数据还隔着整个安全认证体系。顺带说一个细节身份证这类芯片卡通常还实现了 ISO 14443-4 传输层所以在 Android 里你还会看到IsoDep这个技术标签。它可以用来收发 APDU 指令但同样只承担传输功能不解决认证问题。后面写代码时会看到怎么区分。2.3 身份证读卡器本质上是什么SAM 才是“读卡”的真身你现在可以去观察一个真实的身份证读卡器不管是有线的 USB 桌面款还是手持蓝牙款拆开之后里面几乎都有一块独立的安全模块业界通常叫 SAMSecure Access Module。它可能是一颗小芯片也可能是一张类似 SIM 卡的东西插在设备主板上。读卡流程是这样的终端 App 发出读卡请求读卡器 MCU 转发给 SAMSAM 与身份证执行双向认证和密钥协商协商通过后卡片才把加密数据吐出来SAM 再负责解密最后把明文通过串口或蓝牙返回给 App。所以结论很清楚没有 SAM 的 Android 手机哪怕 NFC 芯片再先进也只是“天线 协议栈”不可能读懂身份证的业务数据。如果某个项目号称“手机 NFC 直读身份证”你可以直接质疑它的 SAM 在哪里。绝大多数可落地的产品形态其实是“手机 App 蓝牙/USB 外置身份证读卡器”App 调厂商 SDK 拿结果。这个意识必须在一开始就建立否则后面选型、评估工作量都会走偏。3. 用安卓 NFC 对身份证卡做协议探测最小可复现代码3.1 声明 NFC 权限与卡片技术过滤器先做一个最小的探测 App目标不是读取身份证明文而是验证一件事你的手机能不能在协议层识别到这张卡。这一步跑通了后面接 SAM 外设才有意义。在 AndroidManifest.xml 里加上 NFC 权限和特性声明uses-permission android:nameandroid.permission.NFC / uses-feature android:nameandroid.hardware.nfc android:requiredtrue /然后在 Activity 上配置 intent-filter。注意身份证不是 NDEF 卡不要用android.nfc.action.NDEF_DISCOVERED要用android.nfc.action.TECH_DISCOVERED并在 XML 里声明我们只关心NfcB和IsoDep两类技术。intent-filter action android:nameandroid.nfc.action.TECH_DISCOVERED / /intent-filter meta-data android:nameandroid.nfc.dynamic_technologies android:resourcexml/nfc_tech_filter /res/xml/nfc_tech_filter.xml内容如下resources xmlns:xliffhttp://schemas.android.com/apk/res-auto tech-list techandroid.nfc.tech.NfcB/tech /tech-list tech-list techandroid.nfc.tech.IsoDep/tech /tech-list /resources这里写两个 tech-list 的意思是只要 Tag 暴露了NfcB或IsoDep任一技术系统就把这个 tag 分发给我们的 Activity。为什么不只写 NfcB因为部分设备对 Type B 卡会先暴露 IsoDep 上层技术过滤条件写窄了反而漏事件。这是实测里最容易踩的坑。3.2 用 ReaderMode 接管卡事件推荐做法依赖 intent-filter 有个问题手机靠近卡片时系统会弹“打开某个 App”的选择框体验很差。更可靠的方式是用NfcAdapter.enableReaderMode()直接在当前 Activity 里接管 NFC 事件这样不弹框、响应也快。代码如下class NfcProbeActivity : AppCompatActivity() { private var nfcAdapter: NfcAdapter? null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) nfcAdapter NfcAdapter.getDefaultAdapter(this) if (nfcAdapter null) { Log.e(TAG, 设备没有 NFC 硬件后面不用测了) } } override fun onResume() { super.onResume() nfcAdapter?.enableReaderMode( this, ::onTagDiscovered, NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) } override fun onPause() { super.onPause() nfcAdapter?.disableReaderMode(this) } private fun onTagDiscovered(tag: Tag) { val nfcB NfcB.get(tag) if (nfcB null) { Log.w(TAG, 这张卡不是 Type Btech tag.techList.joinToString()) return } try { nfcB.connect() val pupi tag.id val appData nfcB.applicationData nfcB.close() Log.i(TAG, PUPI${pupi.toHex()}) Log.i(TAG, AppData${appData.toHex()}) } catch (e: IOException) { Log.e(TAG, 连接或传输异常, e) } finally { if (nfcB.isConnected) { nfcB.close() } } } private fun ByteArray.toHex(): String joinToString() { %02X.format(it) } }这段代码的逻辑很清晰在onResume里开启 ReaderMode声明只监听 Type B 和跳过 NDEF 检查当卡片靠近时系统回调onTagDiscovered我们拿到NfcB实例后读取 PUPI 和 applicationData也就是物理层能暴露的全部信息。几个参数说明一下FLAG_READER_NFC_B告诉 NFC 驱动只启用 Type B 检测减少无关干扰。FLAG_READER_SKIP_NDEF_CHECK跳过 NDEF 解析流程。身份证不是 NDEF 卡如果不加这个 flag系统会花时间去尝试解析 NDEF导致回调变慢甚至不回调。tag.id对 Type B 卡来说通常是 PUPI是一个协议层的临时标识不是证件号也不是任何业务主键。nfcB.applicationData拿到的只是 ATQB 里的应用数据字段同样不具备业务含义。跑通之后你会看到日志里出现一串十六进制字节。如果你拿身份证测试这串字节很可能每次都一样如果你拿银行卡测试也可能长得很类似。这说明什么说明协议层无法区分卡种。不要再往下猜了更不要试图靠 ATQB 去“识别身份证”这条路没有业务价值。3.3 区分 Type A 与 Type B 的自测方法想验证手上这张卡到底走什么协议最直接的办法是把tag.techList打出来。private fun onTagDiscovered(tag: Tag) { Log.i(TAG, TechList: tag.techList.joinToString(,)) }常见的输出有几种门禁卡NfcA,MifareClassic或NfcA,MifareUltralight说明是 Type A。身份证或银行 IC 卡NfcB,IsoDep或NfcB,NfcV这类组合说明是 Type B 体系。某些兼容卡同时出现NfcA,IsoDep代表这张卡走了 Type A 的 ISO 14443-4 上层。一个小建议用你手边能确认的卡各测一遍。找一张物业门禁卡测再看一张银行卡测最后拿身份证测把三次的 TechList 对比一下你对协议层的理解会比看十篇文章都清楚。这一步做完你就知道“有没有识别出身份证”这个问题本身就不该问——协议层没有这个能力只有 SAM 有。4. 避坑 / 常见问题 / 排查让“读身份证”项目不再翻车的 5 个细节4.1 贴卡没任何回调天线位置和 ReaderMode 的坑现象身份证放在手机背面App 毫无反应logcat 里连 Tag 事件都没有。原因八成是手机 NFC 天线不在你贴的那个位置。很多手机天线在摄像头模组附近不在手机正中央还有一个常见问题是onPause里调用了disableReaderMode但系统在某些 ROM 上会在界面切换时自动重置 ReaderMode 配置导致你其实没在监听。解决先查手机说明书上的 NFC 天线位置把卡贴在天线中心区域再在onTagDiscovered入口打一行 Log确认回调到底有没有进来。如果没进来检查你enableReaderMode的 flag 里是不是只写了FLAG_READER_NFC_B而这张卡实际是从 IsoDep 暴露的。4.2 报“Tag Lost”场强和卡片摆放的问题现象日志里已经出现 Tag 事件但紧接着又报TagLostException或者前 0.5 秒能连上后 0.5 秒就断了。原因Type B 对场强的要求比 Type A 敏感手机壳、金属贴片、手指盖住天线都会让供电不稳定卡片悬空、只压到一个角也会出现类似问题。解决把卡平放在桌面上手机完全覆盖上去并静止 1 秒钟。不要手抖不要一边移动一边读。这个问题在你接入外置 SAM 读卡器后会消失因为专业读卡器的天线面积和供电设计是专门优化的手机自带天线只能做到“能探测”做不到“在移动中稳定握持”。4.3 好不容易读到数据却以为那是身份证号现象日志里打出了一串十六进制比如541A6F33C2D1开发或项目经理拿着这串数字兴奋地说“这就是身份证号”。原因这是把物理层标识当业务数据了。PUPI 是协议层的伪唯一标识每次通信可能出现也可能固定但跟身份证号码没有任何映射关系。拿它去对接业务系统等于拿网卡 MAC 地址当用户名能跑通但毫无意义。解决明确告诉团队协议层探测只用于“这张卡是不是 Type B / 能不能建立连接”这一件事。业务字段必须来自 SAM 解密后的明文结构二者不能混用。4.4 下载的“源码”一编译就崩它根本不是手机 NFC 项目现象从网上找到的“安卓 NFC 读身份证源码”导入 Android Studio 后要么缺少 .so 库要么一堆类找不到要么运行时直接闪退。原因这类项目里大部分是 SAM 厂商的 SDK 工程它默认你要有一台外置读卡器硬件和一个授权密钥没有这两样代码跑不起来。还有一类更坑作者把读卡器的串口通信代码抄过来改了包名就发出来连 NFC 权限都没声明。解决下载任何源码后第一件事看 build.gradle 里的依赖和三方库列表。如果里面有usb、serial、bluetooth这类设备通信库说明它是“外设方案”不是“手机 NFC 方案”如果没有依赖但声称能读身份证那基本就是模拟数据。别浪费时间调编译直接换正规厂商 SDK。4.5 在后台服务里轮询 NFC导致系统 NFC 服务卡死现象App 退到后台程序通过后台服务循环扫描 NFC结果系统 NFC 开关变灰其他 App 也用不了了。原因enableReaderMode设计上是给前台界面用的它要求注册者是当前活跃的 Activity 或前台服务。你在后台拿着 NFC 适配器不放会在系统服务层引发资源锁冲突严重时整个 NFC 堆栈挂掉。解决让 NFC 探测和 UI 生命周期绑定。要常驻就做一个前台服务并维护一个可见的通知同时把 ReaderMode 回调放在服务中不要用普通后台服务配合while(true)轮询。这条是我在实际项目里踩过的当时某公司的模拟项目X里负责的同事把 NFC 监听写在了后台线程里结果用户手机 NFC 死到需要重启机器才恢复线上反馈一片。5. 如果真的要交付“安卓读身份证”功能合规可行的三层架构5.1 方案选型手机 NFC 直读 vs 外置读卡器先看表格再解释为什么。维度手机 NFC 直读外置身份证读卡器蓝牙/USB能否读到明文不能只有物理层标识能SAM 解密后返回明文硬件成本手机自带零额外成本需采购一台读卡器数百到上千元不等开发复杂度低但无业务价值中等需对接厂商 SDK 和硬件调试适用场景卡存在性检测、协议验证访客登记、实名认证、自助终端、柜台业务选型建议很简单只要业务需要展示姓名、证件号这类字段就不要在“手机 NFC 直读”上浪费时间。手机 NFC 在这条链路里最多扮演“发现卡片”的角色真正的读卡必须交给带 SAM 的外设。反过来如果你的业务只需要判断“用户有没有带这张卡在身”那手机 NFC 直读反而够用甚至还能节省一整套外设成本。5.2 App 与外置读卡器的对接方式常见做法是找一家有授权资质的读卡器厂商拿他们的 SDKApp 通过蓝牙或 USB OTG 连接读卡器然后调用readCard这类接口回调里直接拿到解析好的姓名、性别、出生日期、住址、证件号等字段。流程代码写出来大致是这样// 厂商 SDK 通常封装了连接与读卡两个动作 val reader IdCardReaderManager.getInstance() fun connectAndRead(deviceAddress: String) { reader.connect(deviceAddress) // 蓝牙 MAC 或 USB 设备节点 .flatMap { reader.startReadCard() } // 触发 SAM 与卡片认证 .subscribe({ idCard - // idCard 已经是解密后的对象 runOnUiThread { tvName.text idCard.name tvIdNumber.text idCard.idNumber } }, { t - Log.e(TAG, 读卡失败通常是因为卡没放平或 SAM 鉴权失败, t) }) }这段代码里的IdCardReaderManager和IdCard是厂商 SDK 的抽象不同厂商的 API 名称和字段类型会有差异。需要注意的几个点各家 SDK 返回的字符串编码不统一有的是 GBK有的是 UTF-8拿到后先归一化再用不然中文姓名会乱码。证件号里的 X 有时是大写有时是小写入库前统一。读卡超时一般设置在 3 到 5 秒超过这个时间卡片可能已经离开射频场不要无限等待。厂商 SDK 一般会自带一段授权校验SDK 的授权文件过期后即使硬件在手也读不出数据这种坑在集成测试阶段很容易遇到。5.3 你作为开发者要守住的边界身份证信息属于高度敏感的个人信息做这个方向时我会在技术选型之外强制加几条底线第一信息只在用户主动配合的流程里读取。比如访客登记用户把身份证放到读卡器上并确认授权App 才能发读卡指令不能做成后台默默扫描。第二日志必须脱敏。姓名可以留证件号必须打码前 6 位和后 4 位展示中间统一隐藏。这在联调阶段特别容易忽略因为开发时为了对数据方便直接把完整字段打进了 logcat一提交测试就被打回。第三明文数据不要裸传到自建服务器。如果业务确实需要云端比对至少要走加密通道并且服务端不要留存原样快照。很多团队图省事把整条身份证数据 POST 到业务接口这是给自己埋雷。第四测试时用测试卡不要用真实身份证反复试验。真实卡每次被读都会留下 SAM 鉴权记录受控环境下没有关系但你拿同事的卡在办公区反复读会触发安全告警别问我怎么知道的。6. 进阶验证在不动身份证明文的前提下验证你的 NFC 链路是否达标最后分享一个我常用的验证技巧专门用来判断“协议探测层”到底做没做对。拿一张废旧的 Type B 卡片放在透明胶带固定的桌面上然后手持手机从离卡 10 厘米处慢慢匀速靠近观察回调产生的距离和每次回调的稳定性。如果靠近过程中onTagDiscovered只触发一次且 PUPI 稳定说明链路正常如果来回靠近时断时续大概率是 ReaderMode 的 flag 配置有问题或者系统 NFC 服务被其他应用抢占。这一步可以在不接触任何敏感数据的情况下把工程里的 NFC 基础层验证到可靠状态。另一个习惯是定期用系统自带的 NFC 信息页面做对照。大多数手机都有内置的 NFC 标签信息展示功能用它读同一张卡看它列出的技术列表和你的 App 打出来的 TechList 是否一致。不一致说明你的过滤条件写偏了。我在模拟项目X里帮某公司做门禁联动验证时一度以为自己已经把身份证读取调通了因为日志里 8 字节的 PUPI 看着很有规律反复呈现某种递增结构。等到我把完整流程交付给客户的开发同学对方问“SAM 模块的授权文件在哪”时我才意识到自己读了一整周的只是物理层标识跟业务数据没有任何关系。那个周末我重新读了协议文档把方案改成外置读卡器路线两天就真正跑通了。这个习惯我一直留到现在接到“读身份证”需求第一句话永远先问“SAM 在哪”。希望这篇文章也能帮你在起步时就想清楚这个关键问题。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑