Android NFC实战:手把手教你读取公交卡、门禁卡数据(附完整Kotlin代码)
Android NFC实战深入解析公交卡与门禁卡数据读取技术每次乘坐地铁时刷卡嘀的一声背后隐藏着怎样的数据交互作为开发者我们能否利用手机NFC功能破解这些日常卡片的数据奥秘本文将带您深入NFC技术腹地从字节层面解析各类卡片的数据结构。1. NFC技术基础与开发环境搭建NFC技术看似简单实则包含复杂的射频通信协议栈。与蓝牙、Wi-Fi等无线技术不同NFC的工作距离严格限制在10厘米以内这种近距离特性使其特别适合安全敏感的场景。环境准备要点硬件要求确认设备支持NFC功能主流Android手机自2015年后基本都配备NFC芯片权限配置在AndroidManifest.xml中添加必要声明uses-permission android:nameandroid.permission.NFC / uses-feature android:nameandroid.hardware.nfc android:requiredtrue /API级别建议最低支持API 19Android 4.4以获得完整的NFC功能支持常见误区提醒很多开发者会忽略uses-feature声明这可能导致应用被安装在不支持NFC的设备上引发运行时错误。2. 卡片类型识别与数据处理框架当NFC天线检测到卡片靠近时Android系统会通过Intent分发机制通知应用。我们需要重点关注三种IntentACTION_NDEF_DISCOVERED处理标准NDEF格式数据ACTION_TECH_DISCOVERED处理特定技术类型的卡片ACTION_TAG_DISCOVERED作为兜底处理方案实战建议对于公交卡、门禁卡这类专用卡片通常需要处理ACTION_TECH_DISCOVERED因为它们大多使用厂商私有格式而非标准NDEF。override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) when(intent.action) { NfcAdapter.ACTION_TECH_DISCOVERED - { val tag intent.getParcelableExtraTag(NfcAdapter.EXTRA_TAG) tag?.let { processTag(it) } } } }3. 深入卡片数据结构解析不同厂商的卡片采用不同的存储结构下面以常见的Mifare Classic卡为例Mifare Classic存储结构区块容量用途Sector 016字节包含UID和厂商信息Sector 1-1548字节/扇区数据存储区Trailer Block16字节/扇区包含密钥和访问控制位读取UID的Kotlin实现fun getCardUID(tag: Tag): String { return tag.id.joinToString() { %02X.format(it) } }安全提示虽然可以读取卡片UID但许多现代卡片会使用随机UID来保护用户隐私因此不要依赖UID作为唯一标识。4. 高级数据处理与异常处理实际项目中会遇到各种异常情况需要健壮的错误处理常见问题处理方案卡片不支持检测到卡片后立即检查技术类型val techList tag.techList if (!techList.contains(android.nfc.tech.MifareClassic)) { showToast(不支持的卡片类型) return }读取超时设置合理的超时时间val mfc MifareClassic.get(tag) try { mfc.connect() mfc.timeout 3000 // 3秒超时 // 读取操作... } catch (e: IOException) { // 处理异常 } finally { mfc.close() }数据校验对读取的数据进行CRC等校验性能优化技巧使用前台分发系统提高响应速度批量读取时重用Tag对象避免在主线程执行耗时操作5. 实战公交卡余额解析虽然各城市公交卡格式不同但大多遵循相似的结构。以下是解析框架选择应用发送SELECT APDU指令选择支付应用验证密钥使用行业密钥进行认证读取余额从特定扇区读取数据并转换注意直接读取金融卡片可能违反相关法规建议仅用于学习研究。fun readBalance(tag: Tag): Double { val mfc MifareClassic.get(tag) try { mfc.connect() val auth mfc.authenticateSectorWithKeyA(1, MifareClassic.KEY_DEFAULT) if (!auth) return 0.0 val block mfc.readBlock(4) // 实际解析逻辑因卡片类型而异 return ((block[12].toInt() shl 8) or block[13].toInt()) / 100.0 } finally { mfc.close() } }开发这类功能时建议准备多张不同类型的卡片进行测试包括公交卡不同城市门禁卡125kHz和13.56MHzNFC标签NTAG系列每次卡片读取操作都应该记录完整的字节数据这有助于逆向分析数据结构。可以使用如下方法打印完整数据fun dumpTagData(tag: Tag) { val mfc MifareClassic.get(tag) val sb StringBuilder() try { mfc.connect() for (sector in 0 until mfc.sectorCount) { if (mfc.authenticateSectorWithKeyA(sector, MifareClassic.KEY_DEFAULT)) { for (block in 0 until mfc.getBlockCountInSector(sector)) { val absoluteBlock mfc.sectorToBlock(sector) block val data mfc.readBlock(absoluteBlock) sb.append(Sector $sector, Block $block: ) .append(data.toHexString()).append(\n) } } } Log.d(NFCDump, sb.toString()) } finally { mfc.close() } }