☰
安卓NFC读身份证:协议边界与SAM授权真相
2026/10/14 18:59:24 网站建设 项目流程

简介:面向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 里包含 PUPI(Pseudo-Unique PUPI Identifier)、应用数据、协议信息等字段。在 Android 里,Tag.getId()拿到的就是这张卡唤醒时返回的 PUPI,NfcB.getApplicationData()拿到的是 ATQB 中的应用数据部分。

这些数据能证明什么?只能证明“这是一张 Type B 卡片,且它的应用数据长这样”。它证明不了这张卡是身份证,因为银行卡、部分电子证件、某些门禁卡也都是 Type B。这也是协议层的局限:物理层设计出来是通用的,不携带发卡机构认知。很多草台班子拿一个 NfcB 的 Demo 就宣称“已跑通身份证读取”,其实只是把 ATQB 打了出来,离真实业务数据还隔着整个安全认证体系。

顺带说一个细节:身份证这类芯片卡通常还实现了 ISO 14443-4 传输层,所以在 Android 里你还会看到IsoDep这个技术标签。它可以用来收发 APDU 指令,但同样只承担传输功能,不解决认证问题。后面写代码时会看到怎么区分。

2.3 身份证读卡器本质上是什么:SAM 才是“读卡”的真身

你现在可以去观察一个真实的身份证读卡器,不管是有线的 USB 桌面款,还是手持蓝牙款,拆开之后里面几乎都有一块独立的安全模块,业界通常叫 SAM(Secure Access Module)。它可能是一颗小芯片,也可能是一张类似 SIM 卡的东西插在设备主板上。读卡流程是这样的:终端 App 发出读卡请求,读卡器 MCU 转发给 SAM,SAM 与身份证执行双向认证和密钥协商,协商通过后卡片才把加密数据吐出来,SAM 再负责解密,最后把明文通过串口或蓝牙返回给 App。

所以结论很清楚:没有 SAM 的 Android 手机,哪怕 NFC 芯片再先进,也只是“天线 + 协议栈”,不可能读懂身份证的业务数据。如果某个项目号称“手机 NFC 直读身份证”,你可以直接质疑它的 SAM 在哪里。绝大多数可落地的产品形态,其实是“手机 App + 蓝牙/USB 外置身份证读卡器”,App 调厂商 SDK 拿结果。这个意识必须在一开始就建立,否则后面选型、评估工作量都会走偏。

3. 用安卓 NFC 对身份证卡做协议探测:最小可复现代码

3.1 声明 NFC 权限与卡片技术过滤器

先做一个最小的探测 App,目标不是读取身份证明文,而是验证一件事:你的手机能不能在协议层识别到这张卡。这一步跑通了,后面接 SAM 外设才有意义。

在 AndroidManifest.xml 里加上 NFC 权限和特性声明:

<uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" />

然后在 Activity 上配置 intent-filter。注意身份证不是 NDEF 卡,不要用android.nfc.action.NDEF_DISCOVERED,要用android.nfc.action.TECH_DISCOVERED,并在 XML 里声明我们只关心NfcB和IsoDep两类技术。

<intent-filter> <action android:name="android.nfc.action.TECH_DISCOVERED" /> </intent-filter> <meta-data android:name="android.nfc.dynamic_technologies" android:resource="@xml/nfc_tech_filter" />

res/xml/nfc_tech_filter.xml内容如下:

<resources xmlns:xliff="http://schemas.android.com/apk/res-auto"> <tech-list> <tech>android.nfc.tech.NfcB</tech> </tech-list> <tech-list> <tech>android.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 B,tech=" + 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 与外置读卡器的对接方式

常见做法是:找一家有授权资质的读卡器厂商,拿他们的 SDK,App 通过蓝牙或 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 在哪”。希望这篇文章也能帮你在起步时就想清楚这个关键问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询