资讯详情

安卓NFC读身份证源码实战:破解Type B兼容与云解码全流程

📅 2026/10/14 12:31:01 | 华诺云谱 👁 阅读
安卓NFC读身份证源码实战:破解Type B兼容与云解码全流程
简介这是一份面向Android开发者的NFC身份证读取项目源码利用手机NFC模块与二代身份证RFID芯片通信结合云端解码服务解析姓名、身份证号等个人信息适合酒店入住、安检、会议签到等身份核验场景。资源包共458个文件压缩后约15.41MB涵盖Java主程序、Gradle构建配置、Android界面资源、CMake原生工具链、Tesseract识别字库及网络请求库等从页面布局到HTTP云解码请求均有完整实现。关键类包括监听NFC事件的NFCReaderActivity、处理数据收发与解析的CloudDecodeService并附有数据加密、权限校验等安全示例帮助开发者理清本地读取-云端解码-结果回显的完整链路。目前已有3609人学习适合具备Android基础并希望掌握NFC、身份证云解码技术的开发者工程目录清晰便于二次开发和功能集成。1. 安卓NFC读身份证这套源码把“读不出来”变成“秒出卡号”手持安卓机贴近身份证背面日志窗口疯狂刷“Tag lost”NFC天线位置换了好几个角度还是没反应——这种场面在自助终端、酒店入住、访客登记这类项目里太常见了。安卓NFC读身份证这件事难的不是调API而是身份证用的ISO 14443 Type B协议在国产手机上兼容性时好时坏加上身份证数据本身就是加密的手机端读不到明文必须配合身份证云解码服务才能拿到姓名和证件号。这套源码包就是干这个的把射频层读取、APDU指令、云解码对接完整串起来适合正在做门禁、政务终端、酒店自助机的开发者直接改。2. 读卡为什么这么难从射频协议到安卓系统栈的层层限制2.1 身份证读卡与普通NFC卡的本质差别身份证走的是ISO 14443 Type B协议而我们日常接触的门禁卡、公交卡大多是Type A。两者在载波调制方式、防冲突机制上完全不同Type A用Miller编码Type B用NRZ编码Type B的通信速率固定为106 kbps且卡片上电后必须先回应ATQBAnswer To Request Type B而不是像Type A那样回应ATQA。手机NFC模块如果只是按Type A的逻辑去轮询身份证贴上去完全不会有反应。另一个更关键的限制是身份证内的数据文件DG1、DG2等在出厂时就是用专有密钥加密的读EF_DG1包含姓名、性别、出生日期、证件号之前必须完成BACBasic Access Control或PAPassive Authentication鉴权。普通手机端没有放置身份证解密密钥的SE安全模块所以手机只能读到UID和ATQB返回的协议参数读不到明文。这就是为什么整套方案必须拆成两段手机端负责物理层通信身份证云解码服务端负责用密钥完成鉴权和解密。Android系统层面对Type B的支持也有历史包袱。NFC相关的类从Android 2.3就有了但NfcB这个类要等到Android 4.4才稳定而且很多国产ROM对Type B的轮询做了裁剪——常见做法是系统NFC服务始终在轮询Type AType B的tag发现延迟明显。我一般会先跑一遍tag.getTechList()看看机器到底认不认Type B不认的话后面全部白搭。2.2 安卓NFC栈的适配边界与系统版本差异读身份证的完整链路是这样的NfcAdapter发现Tag → 系统回调newIntent()→ 获取Tag对象 → 通过NfcB或NfcA连接卡片 → 发送APDU指令 → 解析返回数据 → 组织云解码请求。其中每一步都可能出问题源码包的设计也是按这个链路来的。// 判断Tag支持的技术栈决定走Type A还是Type B Tag tag intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); String[] techList tag.getTechList(); for (String tech : techList) { Log.d(NFCTag, supported tech: tech); } if (Arrays.asList(techList).contains(NfcB.class.getName())) { // 走Type B流程 NfcB nfcB NfcB.get(tag); } else { // 部分国产机Type B识别延迟这里需要重试机制 }这段代码在Android 4.4到Android 15上都能跑但不同版本有细微差别Android 8.0之后前台分发系统的enableForegroundDispatch()对Intent过滤要求更严格Android 12之后如果targetSdkVersion设为31必须声明android.permission.NFC之外的运行时权限检查Android 14开始读取NFC Tag不再需要READ_PHONE_STATE权限但如果你代码里引用了旧版判断逻辑会在部分机型上直接闪退。注意参数NfcAdapter.EXTRA_TAG是在onNewIntent()里取的不是getIntent()。Activity如果是singleTop启动模式getIntent()拿到的往往是上一次的Intent对象这是新手最容易犯的错。2.3 云解码的关键链路UID读取到明文下发的完整流程手机端读身份证只能拿到两个东西UID卡唯一标识和ATQB返回的协议信息。云解码的做法是手机端把UID、卡产生的随机数、以及从卡片读取的部分加密数据块拼装成请求发送到身份证云解码服务端服务端用持有的密钥完成BAC鉴权解密出DG1文件内容再返回姓名、性别、出生日期、住址、证件号。源码里对应的是这样一条调用链// 读取UID 发起云解码请求 byte[] uid tag.getId(); String cardUid bytesToHex(uid); String deviceNo getDeviceSerial(); // 设备编号 String timestamp String.valueOf(System.currentTimeMillis() / 1000); String sign generateSign(deviceNo, cardUid, timestamp); // 签名 JSONObject requestBody new JSONObject(); requestBody.put(deviceNo, deviceNo); requestBody.put(cardUid, cardUid); requestBody.put(timestamp, timestamp); requestBody.put(sign, sign); // 发送到云解码服务端等待返回明文这里有几个参数值得解释deviceNo是云解码服务分配的设备标识每个终端一个服务端靠它做限流和审计timestamp单位是秒服务端一般会校验与当前时间的偏差超过5分钟直接拒绝sign的算法通常是MD5(deviceNo cardUid timestamp secretKey)注意拼接顺序必须与服务端约定一致否则永远返回签名错误。云解码的SDK包一般会封装好HTTP请求和JSON解析但源码包为了演示完整流程直接裸写了HttpURLConnection的请求代码方便开发者看明白底层。实际项目中如果追求稳定建议换成OkHttp并加上连接池和超时重试。3. 源码落地从导入工程到跑通读卡全流程3.1 源码包结构与工程导入拿到源码包之后先看目录再动手。这个包的结构是标准的Android Studio工程格式分为app模块和nfc_sdk模块后者是可以直接打进aar的独立库。文件清单如下路径作用app/src/main/java/.../MainActivity.java读卡主界面处理Intent分发app/src/main/java/.../NfcReaderHelper.java核心读卡逻辑封装Type B通信app/src/main/java/.../CloudDecoder.java云解码请求与响应解析app/src/main/AndroidManifest.xml权限声明与Activity配置nfc_sdk/src/main/java/.../NfcReaderManager.javaSDK入口类面向二次开发导入步骤很常规Android Studio选择File → New → Import Project选中工程根目录的build.gradle文件等待Gradle Sync完成。需要注意的是SDK模块里引用了身份证云解码的jar包这个jar放在nfc_sdk/libs/目录下如果你换了自己的云解码服务商直接替换jar并修改CloudDecoder.java里的常量即可不需要动NFC通信层的代码。3.2 权限声明与NFC前台分发配置NFC功能的核心权限只有一条android.permission.NFC。但Android 12及以上系统对前台服务有额外要求如果你要长时间监听NFC事件比如自助终端常驻需要声明FOREGROUND_SERVICE权限和对应的前台服务类型。源码包里已经把这套写好了。!-- AndroidManifest.xml 关键配置 -- uses-permission android:nameandroid.permission.NFC / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE / uses-feature android:nameandroid.hardware.nfc android:requiredtrue / activity android:name.MainActivity android:launchModesingleTop android:exportedtrue intent-filter action android:nameandroid.nfc.action.TECH_DISCOVERED / /intent-filter meta-data android:nameandroid.nfc.action.TECH_DISCOVERED android:resourcexml/nfc_tech_filter / /activitylaunchMode必须设为singleTop因为NFC Tag发现时系统会通过onNewIntent()把新的Intent传给已有的Activity实例如果用默认的standard模式每贴近一次卡片就会新建一个Activity导致读卡逻辑重复执行。nfc_tech_filter.xml文件声明了允许捕获的技术栈列表这里是全部Type B相关的技术包括NfcB、NfcF部分银行IC卡用、IsoDep。3.3 核心读卡代码连接、收发APDU指令与数据解析读卡的核心是transceive()方法它负责向卡片发送APDU指令并接收响应。身份证的APDU指令集遵循ISO 7816-4规范常用的指令包括00 A4 04 00选择文件、FF B0 00 04 00 08读取二进制数据。源码包在NfcReaderHelper.java中做了完整封装。// 读取身份证EF_DG1文件的第4字节起8字节数据 private byte[] readBinary(NfcB nfcB, int offset, int length) throws IOException { byte[] cmd new byte[7]; cmd[0] (byte) 0xFF; // CLA专有指令类别 cmd[1] (byte) 0xB0; // INSREAD BINARY cmd[2] (byte) ((offset 8) 0xFF); // P1偏移量高字节 cmd[3] (byte) (offset 0xFF); // P2偏移量低字节 cmd[4] (byte) 0x04; // 文件标识符高位 cmd[5] (byte) 0x00; // 文件标识符低位 cmd[6] (byte) length; // Le期望返回的长度 Log.d(APDU, sent: bytesToHex(cmd)); byte[] response nfcB.transceive(cmd); Log.d(APDU, recv: bytesToHex(response)); // 响应格式最后两个字节是状态码9000表示成功 if (response.length 2) { throw new IOException(empty response); } int statusCode ((response[response.length - 2] 0xFF) 8) | (response[response.length - 1] 0xFF); if (statusCode ! 0x9000) { throw new IOException(APDU error: 0x Integer.toHexString(statusCode)); } // 返回去掉状态码后的有效数据 return Arrays.copyOfRange(response, 0, response.length - 2); }这段代码有两个值得注意的地方。第一是transceive()方法的长度限制很多国产机型的NFC驱动对单次transceive的数据长度有上限常见的是239字节或240字节超过这个长度驱动直接返回IOException这就是业界常说的“240坑”。解决方案是分块读取把EF_DG1文件按8字节或16字节的块多次读取再拼接成完整数据。第二是响应状态码的判断。ISO 7816-4规定返回0x9000表示操作成功其他值都表示异常比如0x6A82表示文件未找到、0x6982表示安全状态不满足。源码包会在状态码非9000时抛出带状态码的异常方便排查是文件选择问题还是鉴权问题。3.4 云解码调用与响应解析的封装方式读出来的EF_DG1原始数据是密文必须交给云解码服务端处理。CloudDecoder.java里封装了完整的请求逻辑包括签名生成、POST请求、响应解析。public DecodeResult decode(String cardUid, byte[] encryptedData) { try { String timestamp String.valueOf(System.currentTimeMillis() / 1000); String sign generateSign(cardUid, timestamp); JSONObject json new JSONObject(); json.put(cardUid, cardUid); json.put(data, Base64.encodeToString(encryptedData, Base64.NO_WRAP)); json.put(timestamp, timestamp); json.put(sign, sign); HttpURLConnection conn (HttpURLConnection) new URL(BASE_URL).openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Content-Type, application/json; charsetutf-8); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); conn.setDoOutput(true); conn.getOutputStream().write(json.toString().getBytes(UTF-8)); int code conn.getResponseCode(); if (code ! 200) { throw new IOException(HTTP code); } String response readStream(conn.getInputStream()); JSONObject result new JSONObject(response); if (result.getInt(code) ! 0) { throw new CloudDecodeException(result.getString(message)); } return new DecodeResult( result.getString(name), result.getString(idNumber), result.getString(gender), result.getInt(birthYear) ); } catch (Exception e) { Log.e(CloudDecoder, decode failed, e); return null; } }这里的generateSign()是签名函数源码包默认实现是MD5(cardUid timestamp secretKey)。云解码服务商不同签名算法也不同有的用HMAC-SHA256有的要求对参数按字典序拼接后再签名。你接入真实服务时一定要先看服务商的接口文档把generateSign()改成对应的算法。另外注意响应里的idNumber返回的是完整18位身份证号这个字段属于敏感个人信息。如果应用场景只需要显示后四位比如访客登记建议在客户端就做脱敏不要传到日志系统也不要存到本地数据库。我一般是在DecodeResult里直接提供一个getMaskedIdNumber()方法返回前6位加后4位的掩码格式。4. 避坑指南五条高频翻车记录与排查清单4.1 坑位总览先知道雷区在哪读卡这件事看起来代码不多但实际跑起来问题五花八门。先把高频坑列出来后面逐条拆解坑位典型症状关键线索transceive 240字节限制读长文件时IOException单次读取长度239即失败Tag lost间歇性掉线贴近卡片后2~3秒断开天线线圈位置偏离云解码签名失败服务端返回sign error参数拼接顺序与约定不符EF_DG1解析偏移错误姓名/证件号乱码TLV解析起点错误Android 12系统兼容闪退或权限弹窗异常运行时权限声明缺失4.2 坑1与坑2transceive 240字节限制与Tag lost现象调用transceive()发送超过239字节的命令或期望接收超过240字节的响应时部分国产手机直接抛出IOException日志里能看到Tag lost或transceive failed。同样一张卡在另一台手机上却正常。原因这是NFC驱动层的限制不是Android API的问题。身份证的EF_DG1文件完整数据接近300字节如果一次性读取必然超过驱动上限。此外部分机型的天线线圈较小身份证这种大尺寸卡贴近后线圈耦合位置偏移超过2~3毫米就会导致信号衰减触发Tag lost。解决读取时按16字节分块每次读取后短暂停顿建议50ms给射频层留出稳定时间。同时把读取重试次数上调到3次每次失败后重新连接NfcB。// 分块读取 重试机制的实现片段 private byte[] readFileChunked(NfcB nfcB, int fileSize) throws IOException { ByteArrayOutputStream buffer new ByteArrayOutputStream(); int offset 0; int retry 0; int chunkSize 16; while (offset fileSize) { try { byte[] chunk readBinary(nfcB, offset, chunkSize); buffer.write(chunk); offset chunkSize; } catch (IOException e) { if (retry 3) throw e; retry; nfcB.close(); nfcB.connect(); // 重新连接后需要重新选择文件 selectFile(nfcB, FILE_ID_DG1); Thread.sleep(50); // 给射频层稳定时间 } } return buffer.toByteArray(); }分块大小chunkSize不是越大越好。实测中16字节比较稳8字节更保险但速度明显变慢。如果你的机型正好卡在边界可以先用8字节小步快跑再逐步加大到16字节找到最优值。4.3 坑3与坑4云解码签名失败与EF_DG1解析偏移错误现象云解码服务端返回“sign error”或“invalid signature”排查了好几遍签名算法都一样。另一种情况是服务端返回了明文但姓名是乱码、证件号缺位。原因签名失败大概率是参数拼接顺序问题。比如服务端要求按deviceNo timestamp cardUid拼接而你按cardUid timestamp deviceNo拼了MD5结果自然不同。还有一种容易被忽略的是字符编码中文参数必须用UTF-8编码后再参与签名计算如果用平台默认编码GBK签名必然对不上。EF_DG1解析偏移错误则是因为TLVTag-Length-Value结构的解析起点写错了。DG1文件的卡数据组Card Data Group里第0个字节是Tag第1个字节是Length后面才是数据值很多代码直接跳过前两个字节去读姓名结果读出来的是长度值。解决签名拼接顺序在接任何一家云解码服务前先用写死的测试参数跑一遍明文请求确认服务端能正确认签名。TLV解析用循环遍历不要用硬编码的偏移量。// 正确的TLV解析方式按Tag逐条读取 private void parseDG1(byte[] dg1Data) { int index 0; while (index dg1Data.length) { int tag dg1Data[index] 0xFF; int length dg1Data[index 1] 0xFF; if (length 100 || index 2 length dg1Data.length) { // 跳到下一个TLV块防止越界 index 2; continue; } byte[] value Arrays.copyOfRange(dg1Data, index 2, index 2 length); switch (tag) { case 0x61: // 姓名标签实际是UTF-8字符串 name new String(value, UTF-8); break; case 0x62: // 证件号标签 idNumber new String(value, UTF-8); break; } index 2 length; } }注意TLV的Tag可能是多字节的如0x5F 0x20这种全限定Tag上面这段代码只处理单字节Tag遇到多字节Tag场景需要先判断tag 0x1F 0x1F再决定是否把下一个字节并入Tag。4.4 坑5与常见误用边界Android 12系统兼容与敏感数据泄漏现象targetSdkVersion升级到31或33后App在部分Android 12机型上闪退或者读卡成功后返回主界面时崩溃。还有一个常见误用把云解码SDK的secretKey硬编码在代码里反编译之后直接泄露。原因Android 12引入了android:exported必须显式声明的要求targetSdkVersion 31如果漏掉这个属性安装时直接报错Android 13新增了POST_NOTIFICATIONS运行时权限如果App有前台服务通知没申请权限会导致服务被杀。解决Manifest里给所有带intent-filter的Activity补上android:exportedtrue云解码的密钥放到服务端代理转发或者用加固方案保护不要迷信源码混淆。!-- Android 12 兼容性关键配置 -- activity android:name.MainActivity android:launchModesingleTop android:exportedtrue android:configChangesorientation|screenSize|keyboardHidden /activityconfigChanges配置也很重要。NFC读卡过程中如果用户旋转了屏幕Activity会重建正在进行的transceive连接直接断开。加上configChanges声明旋转屏幕时Activity不重建保留读卡会话。这属于那种“不做不知道做了才发现救了命”的配置。5. 进阶技巧用NDEF把读到的信息变成可交互卡片读身份证本身是单向过程但你可以把读取结果反向写回NFC标签或手机内置的NDEF功能区做一个“身份证信息缓存卡”。这在门禁场景特别有用访客第一次刷身份证登记后把脱敏后的信息写入一个空白NFC标签或手机NFC模拟区之后再次进门不用掏身份证刷一下标签就能比对身份减轻云解码服务端的调用压力。实现方式是在onNewIntent()里同时处理NDEF消息的读写。注意身份证本身不支持NDEF写入你需要额外放一张空白NFC标签Type 2或Type 4均可。写入时用Ndef.get(tag)拿到连接然后NdefMessage封装要写的数据// 将脱敏后的身份信息写入空白NFC标签 private void writeNdefTag(Tag tag, String maskedId) { Ndef ndef Ndef.get(tag); if (ndef null) { return; } try { ndef.connect(); NdefRecord record NdefRecord.createTextRecord(zh, maskedId); NdefMessage message new NdefMessage(record); ndef.writeNdefMessage(message); } catch (IOException | FormatException e) { Log.e(NdefWriter, write failed, e); } finally { try { ndef.close(); } catch (IOException ignored) { } } }createTextRecord的第一个参数是语言代码zh表示中文内容Android的NFC系统会按这个字段做文本编码提示。写入前最好先检查ndef.isWritable()否则会抛FormatException。标签容量也要看好Type 2标签常见的是180字节一段姓名加证件号掩码足够用。在线读卡稳定的前提下你可以进一步优化云解码的并发参数。CloudDecoder类里的connectTimeout和readTimeout分别设为5000ms和10000ms比较稳妥云解码服务端的响应时间一般不超过3秒但网络抖动时10秒也算正常。重试策略我建议最多2次超过2次直接提示用户稍后再试避免卡在等待界面。验证整个方案的最终手段是找一张测试卡由模拟项目X提供的标准测试卡按照“UID读取 → EF_DG1分块读取 → 云解码 → 明文返回”的链路走一遍确认每个环节的日志输出都符合预期。从那以后我每次做NFC读卡需求都强制走一遍“硬件选型 → 系统版本 → 协议适配 → 云解码 → 错误兜底”这五层检查翻车的概率至少降了七成。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑