先别急着打开Android Studio。我这次做手机模拟门禁卡踩了整整一个晚上的坑最后发现99%的资料都没把最核心的一件事讲透你的门禁卡是什么卡决定了你能不能模拟、怎么模拟。事情的起因很简单小区换了新门禁发的是Mifare Classic卡我这人丢三落四一周补办了三次卡。干脆想能不能把门禁卡塞进手机里NFC都普及这么多年了按理说这是成熟功能。结果一查资料、一上手才发现这里面的水比想象中深。搜遍全网教程不是让你装一堆来路不明的模块就是甩一句“用NFC卡模拟App就行”结果装上要么提示不兼容要么刷开门禁的瞬间被拦在门外——为什么别人的教程就行你就不行因为门禁卡的类型、手机NFC协议栈、门禁读卡器的响应方式三层因素有一层对不上就白搭。这篇文章把我从上到下的完整实现过程扒给你看包含了可直接运行的读卡和卡模拟代码同时把原理边界也掰开揉碎讲清楚。不管你以前有没有接触过Android NFC按这篇的步骤走至少能搞清楚“我这卡到底能不能模拟”“卡在哪一步”“代码该怎么写”。1. 先搞清楚门禁卡是哪一类读不到卡、模拟失败的根源很多人在网上问“为什么我的手机识别不了门禁卡”点开详情一看大概率是他把门禁卡贴到手机背面手机连个反应都没有。这不一定是你代码写得不对而是你手里的卡根本就不是手机NFC能直接交互的那一类。1.1 门禁卡的三大家族市面上常见的门禁卡按通信频率和协议大致分三种卡类型工作频率常见形态手机NFC能不能直接读ID卡EM4100等125kHz低频厚卡、大部分老式门禁扣不能手机NFC芯片只在13.56MHz工作Mifare ClassicS50/S7013.56MHz最常见的薄卡、IC门禁卡能绝大多数安卓手机都支持CPU卡ISO 14443-413.56MHz银行卡、部分新式门禁卡能支持ISO-DEP协议就能交互如果你的门禁卡是125kHz的ID卡那这条路从一开始就断了。手机NFC芯片只工作在13.56MHz频段物理层面就匹配不上。这种卡要么继续用实体卡要么去搞专门的ID卡复制器手机模拟不了。最幸运的情况是Mifare Classic它也是目前小区、写字楼用得最多的一类。不过“能读”和“能模拟”是两回事后面我会专门讲这个边界。1.2 用代码给卡片“验明正身”与其靠猜不如直接读卡片的ATQA和SAK值。这两个参数是ISO 14443-3协议里卡片在防碰撞阶段回给读卡器的关键信息决定了卡片的技术类型。你可以写一个最简单的读取器App把下面这段逻辑跑一遍private fun resolveCardType(tag: Tag): String { val techList tag.techList techList.forEach { Log.d(TAG, 支持技术: $it) } return when { techList.contains(MifareClassic) - Mifare Classic (S50/S70) techList.contains(IsoDep) - ISO 14443-4 / CPU卡 techList.contains(NfcA) - ISO 14443-3A techList.contains(NfcB) - ISO 14443-3B techList.contains(NfcF) - FeliCa else - 未知卡型 } }tag.techList会列出这张卡支持的所有NFC技术比如一张典型的Mifare Classic卡会同时包含NfcA和MifareClassic两项。如果只包含NfcA很可能是Mifare Ultralight或者某些加密卡解析方式完全不同。提示判断卡型的时机必须在NFC连接之前。Tag对象一旦创建techList就固定了不会因为你后续connect而变化。2. 手机NFC的模拟能力边界哪些能App模拟、哪些不能这是全网教程讲得最含糊的部分也是我被坑得最惨的部分。先说结论Android第三方应用通过HCEHost Card Emulation没法直接模拟Mifare Classic门禁卡。这不是代码努不努力的问题是协议栈层面就封死了。2.1 三种“模拟”方案的真实差异方案原理适用卡型门槛系统级卡模拟厂商ROMNFC控制器固件直接模拟卡片UID等小米、华为等系统自带“门禁卡”功能依赖手机厂商全卡模拟RootXposed拦截NFC驱动层收发完整模拟卡片数据理论上支持Mifare Classic需要Root且不同ROM兼容性差HCE应用层模拟手机系统把NFC控制器收到的APDU转发给App仅支持ISO 14443-4 / CPU卡无需Root但卡型受限很多教程让你装“NFC卡模拟”类App其实走的就是HCE。它模拟出来的是一张“支持ISO-DEP协议的卡”不是一张“Mifare Classic”。门禁读卡器在防碰撞阶段发现手机回应的防碰撞参数不对根本不会继续往下走。2.2 为什么HCE模拟不了Mifare UIDNFC交互分三层ISO 14443-3的防碰撞/选卡阶段ISO 14443-4的APDU交换阶段再往上才是NDEF或者Mifare的块读写。门禁读卡器读Mifare Classic时第一步是防碰撞目标是拿到卡片的UID4字节或7字节。很多门禁系统只校验UID就开门——市面上大量复制卡就是这么干的买一张UID可写卡把原卡UID写进去就完事。Android提供给第三方应用的HCE只开放了ISO 14443-4的APDU交换能力。防碰撞阶段由NFC控制器固件处理第三方应用根本拿不到改UID的入口。所以App层想模拟Mifare UID结论是做不到。能实现全卡模拟的只有两条路厂商ROM系统级功能或者Root后替换NFC协议栈。那HCE有没有用有而且很有用。如果你们门禁用的是CPU卡带COS的智能卡读卡器选卡后会发SELECT AID再走APDU认证流程。HCE正好可以在应用层处理这些APDU实现一张虚拟CPU卡。第5章的Demo就是围绕这个做的。3. 工程配置与NFC调用方式权限、ReaderMode和回调初始化不管读卡还是模拟环境配置是第一步。Android的NFC API看起来简单但有几个细节没处理好真机调试时会浪费大量时间。3.1 权限声明与AndroidManifest配置在AndroidManifest.xml里加上这两行uses-permission android:nameandroid.permission.NFC / uses-feature android:nameandroid.hardware.nfc android:requiredtrue /uses-feature带requiredtrue是告诉应用商店“没有NFC硬件的设备别给我装”。如果你的App还有非NFC用途可以改成requiredfalse然后在代码里运行时判断。3.2 用ReaderMode还是前台NDEF调度Android读取NFC标签有几种方式最老土的是在onNewIntent里等NDEF消息但门禁卡大多不带NDEF数据这种方案会漏卡。实测下来最稳的是enableReaderMode它能绕过系统弹窗确认只要检测到NFC标签就直接回调对门禁卡这种纯技术卡来说体验好太多。override fun onResume() { super.onResume() nfcAdapter?.enableReaderMode( this, readerCallback, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_F or NfcAdapter.FLAG_READER_NFC_V or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) } override fun onPause() { super.onPause() nfcAdapter?.disableReaderMode(this) }FLAG_READER_SKIP_NDEF_CHECK很重要。如果不加系统检测到NDEF标签时会先用NDEF解析逻辑处理一遍对Mifare Classic这种非NDEF卡会多出不必要的延迟。3.3 初始化NfcAdapter与权限校验nfcAdapter NfcAdapter.getDefaultAdapter(this) if (nfcAdapter null) { Log.e(TAG, 设备不支持NFC) return } if (!nfcAdapter!!.isEnabled) { Log.e(TAG, NFC未开启请到系统设置打开) }这两个判断一定要做别偷懒。没有NFC硬件的设备上getDefaultAdapter返回的是null直接调用会闪退。4. 读卡代码实战UID、ATQA/SAK和Mifare扇区一次拿走理清原理后真正的编码环节就顺畅了。这一节给的是完整可运行的MainActivity前提是你新建了一个Android工程包名随意只要在activity_main.xml里放一个TextViewid设为tv_result就行。4.1 读取UID、ATQA、SAK与Mifare扇区数据package com.example.nfcreader import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.MifareClassic import android.nfc.tech.NfcA import android.os.Bundle import android.util.Log import android.widget.TextView import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { private val TAG NfcReader private var nfcAdapter: NfcAdapter? null private lateinit var tvResult: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvResult findViewById(R.id.tv_result) nfcAdapter NfcAdapter.getDefaultAdapter(this) if (nfcAdapter null) { tvResult.text 当前设备不支持NFC return } if (!nfcAdapter!!.isEnabled) { tvResult.text NFC未开启 } } override fun onResume() { super.onResume() nfcAdapter?.enableReaderMode( this, readerCallback, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_F or NfcAdapter.FLAG_READER_NFC_V or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) } override fun onPause() { super.onPause() nfcAdapter?.disableReaderMode(this) } private val readerCallback NfcAdapter.ReaderCallback { tag - runOnUiThread { tvResult.text parseTag(tag) } } private fun parseTag(tag: Tag): String { val sb StringBuilder() sb.append(UID: ).append(tag.id.toHex()).append(\n) sb.append(技术支持: ).append(tag.techList.joinToString()).append(\n) // 读取ATQA和SAK try { val nfcA NfcA.get(tag) nfcA.connect() sb.append(ATQA: ).append(nfcA.atqa.toHex()).append(\n) sb.append(SAK: ).append(String.format(%02X, nfcA.sak.toInt() and 0xFF)).append(\n) nfcA.close() } catch (e: Exception) { Log.w(TAG, 读取ATQA/SAK失败, e) } // 读取Mifare Classic数据 if (tag.techList.contains(MifareClassic)) { sb.append(readMifareClassic(tag)) } return sb.toString() } private fun readMifareClassic(tag: Tag): String { val sb StringBuilder() val mfc MifareClassic.get(tag) try { mfc.connect() sb.append(Mifare Classic 扇区数: ).append(mfc.sectorCount).append(\n) val keys arrayOf( MifareClassic.KEY_DEFAULT, MifareClassic.KEY_MIFARE_APPLICATION_DIRECTORY, MifareClassic.KEY_NFC_FORUM ) for (sector in 0 until mfc.sectorCount) { for (key in keys) { if (mfc.authenticateSectorWithKeyA(sector, key)) { val blockCount mfc.getBlockCountInSector(sector) for (blockIdx in 0 until blockCount) { val blockIndex mfc.sectorToBlock(sector) blockIdx try { val data mfc.readBlock(blockIndex) sb.append(String.format(扇区%02d 块%02d: %s%n, sector, blockIndex, data.toHex())) } catch (e: Exception) { sb.append(String.format(扇区%02d 块%02d: 读取失败%n, sector, blockIndex)) } } break } } } mfc.close() } catch (e: Exception) { sb.append(读取MifareClassic失败: ).append(e.message).append(\n) } return sb.toString() } private fun ByteArray.toHex(): String joinToString() { String.format(%02X, it.toInt() and 0xFF) } }4.2 结果怎么看拿小区卡跑一遍你会看到类似这样的输出UID: 6A2B3C4D 技术支持: NfcA, MifareClassic ATQA: 0004 SAK: 08 Mifare Classic 扇区数: 16 扇区00 块00: 4D3C2B6A 12345678 9ABCDEF0 11223344SAK08是Mifare Classic 1K最常见的值4字节UID。SAK18则常见于7字节UID的卡复制或者模拟时要注意这个差异。门口刷卡的逻辑往往只比对UID所以读卡阶段第一行那个UID就是很多门禁系统的“钥匙”。注意扇区末尾的Trailer块每个扇区第3块存的是密钥和访问位当你手上没有对应Key时读取会直接抛异常。上面代码用了三层常见默认密钥依次尝试能读的部分都是门禁系统没做加密处理的读不到也没关系说明该卡有独立密钥体系需要专业的卡务工具才能处理。5. 卡模拟代码实战HCE模拟一张ISO-DEP门禁卡读卡只是前半场后半场的卡模拟才是真正有意思的部分。这一节完整实现一个HostApduService让手机在门禁读卡器面前呈现为一张“支持ISO 14443-4的CPU卡”。5.1 HCE服务声明与AID配置先在res/xml下新建apduservice.xmlhost-apdu-service xmlns:androidhttp://schemas.android.com/apk/res/android android:descriptionstring/service_description android:requireDeviceUnlockfalse aid-group android:descriptionstring/aid_group_description android:categoryother aid-filter android:nameF222222222 / /aid-group /host-apdu-serviceAID是一串偶数位的十六进制数字F222222222对应5字节的AID。category这里用other而不是payment因为门禁卡模拟不属于支付场景避免和支付类HCE冲突。然后在AndroidManifest.xml的application内注册服务service android:name.MyHostApduService android:exportedtrue android:permissionandroid.permission.BIND_NFC_SERVICE intent-filter action android:nameandroid.nfc.cardemulation.action.HOST_APDU_SERVICE / /intent-filter meta-data android:nameandroid.nfc.cardemulation.host.apduservice android:resourcexml/apduservice / /serviceandroid:permissionandroid.permission.BIND_NFC_SERVICE是系统强制要求不能省。5.2 HostApduService实现代码package com.example.nfchce import android.nfc.cardemulation.HostApduService import android.os.Bundle import android.util.Log class MyHostApduService : HostApduService() { override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray { Log.d(HceService, 收到APDU: ${commandApdu.toHex()}) // 门禁读卡器选卡后第一步通常是 SELECT AID if (isSelectAidCommand(commandApdu)) { // 返回9000表示成功选中 return byteArrayOf(0x90.toByte(), 0x00) } // 其他APDU根据实际门禁逻辑处理 return byteArrayOf(0x90.toByte(), 0x00) } override fun onDeactivated(reason: Int) { Log.d(HceService, 卡模拟停用原因: $reason) } private fun isSelectAidCommand(apdu: ByteArray): Boolean { // ISO 7816 SELECT命令格式00 A4 04 00 [长度] [AID] return apdu.size 4 apdu[0] 0x00.toByte() apdu[1] 0xA4.toByte() apdu[2] 0x04.toByte() } private fun ByteArray.toHex(): String joinToString( ) { String.format(%02X, it.toInt() and 0xFF) } }5.3 这个Demo能刷开什么门禁刷不开什么门禁先泼盆冷水这个Demo刷不开绝大多数普通Mifare门禁。因为那些门禁读卡器根本不走ISO 14443-4的AID流程它们只做Mifare防碰撞和UID校验。那它能干什么如果你的门禁系统用的是CPU卡读卡器会做以下几件事在ISO 14443-4层激活手机发送SELECT AID试图读取卡片上的应用通过一系列内部认证APDU比如00 84 00 00 08生成签名来验证卡内数据。第2步正是processCommandApdu的切入时机。你只需要按门禁系统的规范把真实CPU卡的APDU应答逻辑原样实现一遍手机就能伪装成那张CPU卡。这里有个现实前提你得先知道目标卡对每条APDU的应答数据。这通常需要一张合法授权可研究的卡片用IsoDep.transceive()逐条抓取分析。提示真实项目里CPU卡内部往往带有安全认证算法不是简单回一段固定数据就能过。固定返回只适合做POC和协议调试量产方案要么拿到卡商授权要么走系统级SE安全元件方案。6. 真机实测中的差异、坑与合规提醒代码归代码真机环境千奇百怪。我在这个项目里踩过的坑单独列出来能帮大家少走很多弯路。6.1 厂商ROM的NFC实现差异同一个enableReaderMode在不同手机上表现完全不一样。小米、华为部分机型会优先走系统自带的“门禁卡模拟”逻辑导致第三方App的ReaderMode偶尔抢不到标签三星和Pixel则中规中矩按协议标准来。实测下来如果系统有自带NFC门禁卡功能先用它试试能不能模拟成功成功率往往高于第三方App第三方App如果发现标签“刚贴上就被系统吃掉了”可以试试点按屏幕唤醒后重新贴卡或者到系统NFC设置里关掉“优先使用系统读卡器”之类的选项部分ROM叫“NFC读卡器模式”。HCE的AID配置也受ROM影响。个别厂商对categoryother的HCE服务支持有bug表现为手机贴上去没有反应。这时可以试试把android:requireDeviceUnlock改成true强制解锁屏幕再刷或者换一台原生安卓机做对照测试。6.2 读卡距离、天线位置与协议细节门禁卡感应区通常在手机背面摄像头附近不是整个背面都灵敏。读卡时把卡片贴在中上部缓慢移动找天线位置比贴正中间更稳。另外NFC的感应距离很短实测稳定识别距离一般在2-3厘米内卡片和手机之间不要夹厚壳、卡片或金属物。还有一个容易忽略的点某些门禁读卡器只工作在一个固定的防碰撞窗口期。手机HCE从检测到卡到返回响应有一定延迟如果门禁读卡器超时快就可能“刷不上”。把手机亮度锁屏时间拉长提前点亮屏幕贴卡能有效降低超时概率。6.3 关于合法使用的提醒写这类内容必须把丑话说在前面。手机模拟门禁卡目前最现实的应用场景是备份自己合法持有的门禁卡比如小区发给你的卡、公司发的工作证真丢了可以用手机应急开门。未经授权读取或复制他人的门禁卡、绕过管理方门禁系统属于违规甚至违法行为本篇文章提供的代码和技术分析仅限于学习研究和自我授权场景。另外顺带提一句网上流传的所谓“NFC中继”方案本质是把读卡器端采集的数据无线转发到手机端再应答这类设备依赖专用硬件和通信链路不在Android原生开发能力范围内也不适合在这类技术社区展开更不建议碰。6.4 后续扩展思路到这里读卡和HCE模拟这条线已经完整跑通了。如果你想继续深入有几个方向值得玩把读到的Mifare转储文件解析成结构化数据研究访问位和密钥分布对接IsoDep.transceive()抓取CPU卡完整通信日志做一张“会应答的虚拟卡”如果手机支持安全元件SE可以研究将卡片数据写入SE的方案不过这条路通常是厂商定制普通开发者拿不到完整权限。从“读卡”到“模拟”中间最关键的不是代码能力而是把NFC协议栈和卡片的握手流程搞明白。Android的NFC API封装得已经很友好了门槛比底层驱动开发低得多但该懂的协议知识一点也省不了。
企业数字化 ERP 产品动态
相关推荐
JavaWeb课设实战:大学活动管理系统从部署到核心代码解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:12:56
汽车电子与电机控制:从失效模式到故障闭环的工程实践体系 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:12:56
芯片设计方法演化:从CAD绘图到SoC与封装协同 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:12:56
3个实战案例教你搞定网络媒体整合营销,告别域名服务器焦虑 3个实战案例教你搞定网络媒体整合营销,告别域名服务器焦虑 你是不是也被域名解析、服务器配置这些词绕晕了?很多做市场推广的朋友,手里有预算、有创意,一遇到技术落地就头大。别急,我整理了3个真实的 实战案例 ,把 网络媒体整合营销… · 2026/9/28 1:40:09
基于OpenCV与LBPH的人脸识别门禁系统:从Haar检测到阈值调优实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:39:57
OpenCV实战:快速筛选iOS相册中的相似图片 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:39:57
VSCode+STM32开发环境搭建全攻略:从Keil到GDB调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:39:57
CMSIS-DSP库函数实战:从入门到性能优化与避坑指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:39:57
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01
制作网页比较方便的软件怎么选?一文搞懂避坑指南 制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25