ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

uni-app跨端NFC开发实战:从门禁到支付的技术选型与避坑指南

uni-app跨端NFC开发实战:从门禁到支付的技术选型与避坑指南 1. 项目背景与整体设计思路先交代一下背景。我手上这个项目是公司内部的园区一卡通升级原来是一套原生 Android 的读卡 App负责门禁刷卡、食堂扣款、会议室签到这些事。后来业务要求同时覆盖 iOS 和微信小程序团队又不想维护三套代码于是决定用 uni-app 重构。听起来很常规但 NFC 这个模块在整个迁移过程中被我们严重低估了尤其当目标是支持 Mifare Classic 卡片的时候坑比想象中多得多。这篇文章就是把从需求评审到联调上线这段路踩过的坑、走过的弯路、最终验证可行的方案完整写下来给准备做 uni-app NFC 的团队一个参考。标题里写了“从门禁到支付”但先把话说在前面门禁和支付在 NFC 领域的技术底座是完全不同的。门禁大多数走 Mifare Classic 的扇区读写或者仅读取卡号而真正的支付场景走的是金融 IC 卡规范比如 EMV/PBOC或者手机厂商的 Pay 体系安全级别完全不是一个量级。如果你的项目是把一张 Mifare 1K 卡当“电子钱包”用本质上属于封闭环境里的小额储值卡跟银行卡闪付是两码事后面我会专门讲这块的安全设计思路。1.1 一张门禁卡背后的技术构成先通俗地把底层概念捋一遍。我们常说的“刷卡”本质上是读卡器和卡片之间通过射频天线进行无线通信。RFID 是射频识别的总称常见的有低频 125kHz、高频 13.56MHz、超高频 900MHz 这些频段而 NFC 是工作在 13.56MHz 的一种近距离无线通信技术兼容了 ISO 14443 A/B、ISO 15693、FeliCa 等一系列标准。换句话说NFC 是 RFID 的一个子集但标准化程度更高、生态更成熟。Mifare Classic 则是 NXP 公司基于 ISO 14443A 标准实现的一套卡片芯片方案。市面上大量小区门禁、校园一卡通、园区工牌用的就是 Mifare Classic 1K 卡。这张卡内部结构非常有特点总共 16 个扇区每个扇区 4 个块每块 16 字节。第 0 扇区第 0 块是厂商数据区包含 4 字节 UID、校验位和厂商信息这张卡的“身份证号”就在这里每个扇区的最后一个块是扇区尾块存放 KeyA、访问控制位和 KeyB相当于这扇门的钥匙锁结构中间的块则可以存放业务数据。这个结构决定了门禁读卡有两种常见模式一种只读 UID把卡号当成身份标识这种最简单系统里存一个白名单就行另一种要读扇区数据卡片里存着用户编号、有效期、余额甚至楼层权限读卡器拿到数据后还要做密钥认证和解密这明显更复杂。搞清楚你的业务属于哪一种比急着写代码重要得多。1.2 门禁与支付技术底座完全不是一回事很多产品经理提需求的时候会说“一张卡既能开门又能付钱”听起来好像一个 NFC 读写功能就全覆盖了实际上开发层面的难度是成倍增加的。门禁场景对安全的要求是“能验明身份”多数情况下读个 UID 就够了即使要读扇区密钥掌握在物业侧风险可控。支付场景却完全不同。银联闪付、Apple Pay、各种手机 Pay 的本质是把银行卡的敏感数据放在安全芯片 SE 里用 token 化技术和动态密钥完成交易每次支付生成一个一次性的交易凭证。而 Mifare Classic 使用的是 Crypto-1 流密码算法这套算法在多年前就被安全社区公开分析过密钥体系早已不是秘密所以它根本扛不住金融级的安全评估。我在项目里跟业务方反复确认过“支付”到底指什么最终确定是园区食堂的小额储值扣款卡片余额最多几百块。这个场景还能做但绝不能把余额直接存在手机端让前端改写而是要把手机变成“身份凭证”真正的资金操作放服务端。这个认知是整个架构设计的基石建议每个团队在立项前都花半天把需求边界谈清楚。1.3 方案选型三类需求三条完全不同的路我在预研阶段把 NFC 需求拆成了三类每一类的实现路径截然不同技术选型必须先对号入座。需求类型典型场景iOS 可行性Android 可行性uni-app 实现难度只读 UID / 卡号门禁白名单、签到、设备绑定可行CoreNFC 可读取标签标识可行MifareClassic.getUID() 即可低封装插件即可读写 NDEF 数据智能海报、蓝牙配对、配置标签可行CoreNFC 原生支持 NDEF可行Ndef 类可读写中注意数据格式兼容扇区机密读写门禁楼层权限、一卡通余额存储iOS 受限系统未开放扇区访问可行需密钥认证后 readBlock/writeBlock高必须原生插件或 UTS读到这里你应该明白了uni-app 虽然口号是“一套代码多端运行”但 NFC 这种底层硬件能力在不同系统上的开放程度完全不一样写代码前先判断需求属于哪一类能帮你节省大量返工时间。我们项目的核心门禁功能同时涉及 UID 读取和扇区权限判断Android 端可以做完整实现iOS 端最终采用的是读 UID 服务端二次校验的方案这个妥协方案后面详细说。2. 环境准备与关键配置uni-app 开发 NFC 的第一道坎并不是代码本身而是环境配置。很多人跟我一样刚开始以为在 manifest.json 里勾选一个权限就能跑结果真机一测才发现连 NFC 适配器都拿不到。这里面的门道值得花一整章讲清楚。2.1 manifest.json 权限声明我在这里翻过车先说结论uni-app 在 App 端并没有像 uni.scanCode 那样开箱即用的 NFC 接口它需要依赖原生插件或 UTS 插件去调用系统能力。因此 manifest.json 的配置要做到两步声明原生权限同时把插件模块勾上。// manifest.json 的 app-plus 节点里需要做如下配置 app-plus: { modules: { NFC: {} }, distribute: { android: { permissions: [ uses-permission android:name\android.permission.NFC\/, uses-feature android:name\android.hardware.nfc\ android:required\true\/ ] }, ios: { privacyDescription: { NFCReaderUsageDescription: 需要使用NFC功能读取门禁卡/一卡通信息 } } } }Android 端要注意的是NFC 权限在系统里属于 normal 级别权限不需要像定位、相机那样做运行时动态申请但必须确保uses-feature声明了android.hardware.nfc否则部分应用市场会认为你的应用不支持 NFC甚至连安装都不让装。另外如果只是辅助功能可以把required设为true这样商店会主动过滤掉没有 NFC 硬件的设备如果 NFC 只是锦上添花就设false。iOS 端则要复杂一些。除了在 manifest.json 里加NFCReaderUsageDescription描述文案之外离线打包时必须在 Xcode 工程里开启 CapabilityNear Field Communication Tag Reading并且要在 entitlements 文件里配置对应的 key。云打包稍微省心一点但同样需要在打包配置里确认勾选。这个描述文案会被系统在用户第一次触发 NFC 功能时显示务必写清楚用途写得含糊会被审核驳回。2.2 UTS 插件和原生插件怎么选配置好权限之后紧接着就是技术选型。uni-app 生态里做 NFC 有三条路去插件市场买现成的原生插件、自己写原生插件让前端调用、用 UTS 插件把原生能力封装成前端可以直接 import 的模块。我三样都试过简单聊聊各自的感受。插件市场里的现成插件适合快速验证原型通常支持 Mifare Classic 的读写、NDEF 解析这些主流能力价格从免费到几百块不等。但问题在于闭源遇到 bug 只能等作者修而且不同插件的 API 风格差异很大后期换插件等于重写业务层。我们上生产环境前发现一个插件在 Android 13 上读取速度很慢最后被迫换方案这笔时间成本很高。自己写原生插件灵活性最高但开发成本和维护成本也不低每次 uni-app 升级、原生 SDK 升级都要重新打包测试而且团队里得有同时懂 Android、iOS 和 uni-app 生态的人。我们最后用的是 UTS 插件方案核心逻辑用 TS 写可以直接访问 Android 和 iOS 的原生 API又能跟 uni-app 的页面代码共享类型定义算是在灵活性和可维护性之间取了平衡。// UTS 插件内部可以这样封装原生调用前端 JS 层直接 import import { NfcAdapter } from android.nfc; import { Intent } from android.content; export function getNFCAvailable(): boolean { const adapter NfcAdapter.getDefaultAdapter(uni.getSystemInfoSync().platform android ? uni.getActivity() : null); return adapter ! null adapter.isEnabled(); }上面的代码是 UTS 开发的一个示意具体 API 映射要以你依赖的 uni-app 版本和运行环境为准。核心思路是UTS 在编译期会把你写的 TS 转为对应平台的原生代码所以 Android 端能用 Kotlin/Java 的 APIiOS 端能用 Swift/OC 的能力前端只管调用统一封装好的 JS 接口这个模式非常适合 NFC 这种强平台相关的能力。2.3 自定义基座、离线打包与真机调试NFC 的调试比普通页面调试麻烦因为权限和原生模块必须打包进基座才生效。用 HBuilderX 自带的标准基座跑普通项目没问题但涉及 NFC 这类原生能力你必须先做一次自定义基座。操作不复杂在 manifest.json 配置完成后点击“自定义调试基座”构建然后把自定义基座安装到手机运行项目时选择这个基座即可。这里有个特别容易踩的坑改了 manifest.json 的原生配置后如果忘了重新打包基座你会发现无论如何都拿不到 NFC 适配器因为运行时的基座里根本没有打进这个模块。我们团队新人接手项目时至少两次栽在这上面排查半天最后发现只是基座没更新。离线打包则是另一个维度的事。如果你的应用需要通过原生工程做特殊集成比如接入厂商推送、定制系统级能力就会走到离线打包这条路。离线打包时 UTS 插件的使用步骤大致是先在 uni-app 项目中开发好 UTS 插件然后通过 HBuilderX 生成离线打包资源再把资源放入原生工程同时把 UTS 插件对应的原生源码或 aar 引入到工程依赖里。这一步的细节比较多强烈建议参照官方离线打包文档一步步来不要凭记忆操作。我自己的习惯是先用云打包验证功能确认无误后再走离线打包流程这样可以隔离变量、快速定位问题。3. NFC 核心功能实战环境搭好之后真正的硬骨头来了。这一章我会按读 UID、扇区读写、门禁联动、支付安全这四个模块把实际开发中验证过可行的流程完整写出来。3.1 读卡流程从开始扫描到拿回 UID读 UID 是整个 NFC 功能里最基础、也最常用的能力。不管是门禁白名单、设备绑定还是判断卡片是否存在第一步都是先把卡片身份拿到手。Android 端的原生机制是注册一个 Intent 过滤器当 NFC 标签靠近时系统会弹出标签调度把 Tag 对象传递给你的 Activity开发时建议用前台调度PendingIntent方式避免读卡时跳出当前页面。// 示意前端通过 UTS 封装后的插件调用扫描 const nfc uni.requireNativePlugin(NFCModule); nfc.startScan({ mode: UID_ONLY, timeout: 30000 }, (result) { if (result.code 0) { console.log(读到的卡号是, result.cardId); this.cardId result.cardId; } else { uni.showToast({ title: 未识别到卡片, icon: none }); } });上述是业务侧的调用示意真正复杂的是原生层对 Intent 的处理。在原生代码里你要在onNewIntent中接收EXTRA_TAG参数然后Tag.getTechList()判断标签支持的技术类型。如果列表里有MifareClassic就可以强转成 MifareClassic 对象并调用getUID()方法拿到字节数组再转成十六进制字符串供业务使用。一个非常实用的经验不要每次读卡都重新启动 Activity尽量把 NFC 相关逻辑放到单例或常驻的 Service 里用生命周期管理 Intent 的传递。我们早期版本因为 Intent 重复调度导致页面卡顿后来改成了在onResume时执行 enableForegroundDispatch在onPause时 disable彻底解决了问题。读卡期间还要考虑用户体验。用户把卡贴上去可能只停留几百毫秒读卡器必须在这个窗口内完成发现、连接、认证、读取整个流程。建议在页面上做一个明显的“读卡中”状态提示同时在代码里做好超时处理和异常捕获避免碰到一次不支持的卡片就白屏卡死。3.2 Mifare Classic 扇区读写密钥是绕不过去的一关说完了读 UID接下来是重头戏Mifare Classic 的扇区读写。前面提到过1K 卡有 16 个扇区每个扇区 4 个块每个块 16 字节。第 0 扇区第 0 块是厂商数据区存储 UID出厂后绝大多数卡片不可修改每个扇区的最后一块是 Trailer 块存放 KeyA、KeyB 和访问位。要读取某个数据块必须先对所在扇区进行密钥认证否则卡片会直接拒绝访问。密钥认证和读块操作的逻辑示意 1. 拿到 Tag 对象检查是否支持 MifareClassic 2. 连接卡片调用 connect() 3. 指定扇区号调用 authenticateSectorWithKeyA(sector, keyBytes) 4. 认证成功后调用 readBlock(blockIndex) 读取 16 字节数据 5. 处理完数据后及时调用 close() 释放连接这里面最大的坑是密钥。厂家出厂的 Mifare Classic 卡默认密钥通常有两组KeyA 全是FFKeyB 是A0A1A2A3A4A5。如果门禁系统在发卡时没有改写密钥那这张卡的数据等于对所有人敞开任何人拿到卡都能读取甚至改写扇区内容。反过来如果门禁系统已经改写了密钥那你的应用要想读扇区就必须在某个环节拿到这把密钥。安全实践上绝对不要把这些密钥硬编码在前端代码里不管是 uni-app 的 JS 层还是原生插件层密钥写死在包里等于把保险柜钥匙贴在保险柜外面。我们的做法是用户在应用内完成身份认证后服务端根据用户权限实时下发一个加密后的会话密钥App 把这个密钥传给原生模块完成扇区认证会话过期就失效。密钥下发链路本身用 HTTPS 加密密钥不落本地数据库用完后立即从内存中清掉。另外要注意Mifare Classic 的扇区读写效率不高读写过程对时序比较敏感。实测下来如果 App 在读取过程中突然切后台或者系统弹出权限弹窗极容易导致连接中断卡片状态会出错。所以进入读卡页面前一定要提醒用户关闭 NFC 相关系统弹窗并且把页面保持在最前台。我们在代码里还做了重试机制一次认证失败后自动重试两次有效提升了体验。3.3 门禁联动读卡器之外你还要考虑这些门禁场景真正落地时你会发现“读到卡号”只是万里长征第一步。整个链路是用户打开 App - 进入刷卡页面 - 贴卡 - 读取 UID/扇区数据 - 上送服务端校验 - 服务端返回是否开门 - App 调用蓝牙或网络模块触发门禁控制器开锁。这里面任何一环卡住用户就会被关在门外。一个容易被忽略的细节是卡片的去重与轮询。当用户把卡贴在手机背面时NFC 的轮询机制可能会在短时间内触发多次 Tag Discovered 事件。如果代码不做去重服务端会收到连续多个相同的开门请求这不仅会造成重复开门记录还可能触发门禁系统的防重入机制。解决办法很简单在原生层记录上一次读取到的卡号和读取时间500 毫秒内的重复卡号直接忽略。iOS 端的门禁方案我需要再展开说一下。由于 iOS 的 CoreNFC 不支持直接访问 Mifare Classic 的扇区数据只能读取标签的基本信息所以团队最终采用了“读 UID 服务端白名单”的架构。手机端 NFC 读取卡片 UID 后把 UID 和当前用户信息一起上送服务端服务端判断这个 UID 是否绑定在当前用户名下并返回开门指令。这种方案对原生门禁系统比较友好因为大多数门禁系统本来就保存着卡号和用户的关系。但它也有一个明显缺点如果门禁系统还要求检查卡内扇区数据比如有效期、楼层权限那么 iOS 端就无法完整模拟实体卡。我们的妥协方案是iOS 端只做“解绑式”开门即服务端强制校验用户身份不再依赖卡内扇区数据如果卡内扇区数据必须一致那就只能引导用户用实体卡。这个限制要在产品规划阶段就跟业务方讲清楚否则验收时很容易扯皮。3.4 支付场景落地的安全设计思路再回到“支付”这个敏感词。如果业务场景是园区食堂、超市、会议室的小额消费技术上可以参考一卡通电子钱包的做法但我的建议非常明确不要在手机端直接改写卡片余额不要尝试绕过任何密钥体系把资金操作全部放到服务端。合规和安全的设计思路是这样的卡片在系统里只充当身份凭证App 读到的 UID 或扇区用户标识用于确认“你是谁”真正的余额和流水只存在于中心数据库中。扣款流程是用户出示手机上的付款码或者靠近读卡器读卡器将身份信息上送服务端服务端开启事务、校验余额、执行扣款、记录流水再把结果返回给读卡器。卡片本身甚至可以不要余额完全变成一个身份令牌。如果在离线环境必须要用卡片余额比如食堂网络不稳定那也得把卡片当成只读的“余额快照”真正的扣款记录在本地事务表里网络恢复后与中心对账。我们团队最终连这一步都砍掉了因为对账和防抵赖的复杂度实在太高一个小数点错误都可能引发客诉。记住一个原则能用服务端算的钱绝不在前端算能用数据库记的账绝不在卡里记。还要专门提一下 Mifare Classic 本身的安全劣势。它的 Crypto-1 算法在多年以前就被安全社区逆向分析过密钥也早已被公开讨论过这意味着把它用于任何涉及真实资金或高安全权限的系统都需要非常谨慎。正因如此现在越来越多新项目开始用 CPU 卡如 Mifare DESFire、复旦微 FM11S 系列替代 Classic 卡后者支持更复杂的加密算法和文件权限管理。如果你的项目还在选型阶段我强烈建议把 CPU 卡列入备选哪怕单张成本高个一两块钱也远比以后被安全问题逼迫升级划算。4. 高频问题的排查速查表写代码只是开始真机联调才是地狱。这一章我把这段时间积攒的高频问题和排查经验整理成速查表每一个都是实际踩坑换来的。4.1 手机扫描不到卡片先从这四步查起“手机明明支持 NFC为什么就是读不到卡”是群里出现频率最高的问题。我每次排查的顺序固定为四个步骤先看系统 NFC 开关、再看贴卡位置、然后查手机壳和贴膜、最后查应用进程占用。第一步系统 NFC 开关。很多用户买完手机之后 NFC 是默认关闭的尤其部分国产 ROM 会默认关闭以省电。App 要做的是在进入刷卡页时主动检查系统 NFC 状态如果未开启用明确的文案引导用户去系统设置打开而不是只弹一个“未检测到卡片”的提示。第二步贴卡位置。NFC 天线一般在手机背面的上部摄像头附近不同的机型位置差异很大。如果用户习惯把卡贴在手机正中间很可能怎么刷都没反应。比较好的做法是在 UI 上画一个手机示意图标注推荐刷区位置降低用户试错成本。第三步手机壳和贴膜。金属边框、磁吸支架、过厚的硅胶壳都会严重衰减或偏转 NFC 的射频场我有一次调试几个小时最后发现是用户新带了一个金属支架壳。排查时不妨让用户把壳摘了试试。第四步进程占用。部分 App 如果长期占据 NFC 的读卡轮询会导致其他应用无法发现标签。遇到读不到卡的情况尝试清理所有后台应用再测。这一点在定制 ROM 上尤为明显后面单独讲。4.2 Android 厂商适配权限声明之外的大坑Android 系统碎片化是老生常谈NFC 领域更是重灾区。除了前面说的 NFC 权限声明你还得面对厂商自定义的省电策略、自启动限制和弹窗管理。典型的情况是小米、华为、OPPO、vivo 这些厂商默认不允许应用在后台调用 NFC一旦 App 切到后台再回来NFC 功能会失效。开发阶段最直观的体现是把 App 切后台再切回来NFC 扫描就“失灵”了重装才好使其实是原生连接没有正确释放。解决办法是在页面onShow里重新初始化 NFC 适配器onHide里主动释放资源。这里也提醒一下很多团队在反馈里看到“小米手机打包 app 之后为啥没有麦克风权限”“为啥 NFC 用不了”这类问题拉日志一看绝大多数都是因为离线打包时原生权限声明不完整或者运行基座没有重新编译。在验证任何模块问题之前先确认你运行的是不是最新打包的基座和资源。厂商 API 差异还体现在卡片交互上。一些三星、谷歌原生系统机型对 Mifare Classic 的支持很标准但部分国产机型在系统层做过 NFC 轮询优化会导致读卡灵敏度下降。这类问题没法通过应用层代码完全解决只能建议用户在系统设置里关闭 NFC 省电模式同时在读卡页面做明显的引导动画。记得在测试矩阵里覆盖不同厂商的机型至少保证主流品牌各有一台真机。4.3 iOS 端的边界能做什么不能做什么iOS 的 CoreNFC 从 iOS 11 开始支持 NDEF 标签读取到 iOS 13 全面放开读 UID 和部分协议指令已经可用但它对 Mifare Classic 的扇区访问始终没有开放。很多从 Android 转过来的开发者容易想当然以为 iOS 也能一样操作扇区结果发现调系统 API 只能拿到标签的基础标识读不出扇区内容。iOS 端做 NFC 开发前先确认自己的需求落在系统允许的范围内可以读取 NDEF 消息读取 ISO 14443A/B、FeliCa 等标签的基础能力读取支持 ISO 7816 APDU 的卡片部分能力。不可以直接访问 Mifare Classic 的扇区密钥认证和读写。可以但不稳定的情况部分 iOS 版本读 UID 支持很好但在老版本或特定机型上可能遇到偶发失败。所以如果你要同时支持 iOS 和 Android 的 NFC 门禁最务实的方案是前面说的Android 做扇区读写iOS 做 UID 读取 服务端校验。如果业务方硬性要求 iOS 也能完整读扇区那就需要引入一个外接读卡器硬件通过蓝牙或音频口与 iPhone 通信这是当前 iOS 生态里唯一可靠的办法。另外 iOS 的隐私弹窗也容易让人困惑。首次使用 NFC 时系统会弹出NFCReaderUsageDescription对应的授权框这个框不是申请权限而是在告知用户你要使用 NFC 标签读取。如果不弹或者弹完就崩很大概率是 entitlements 或者 plist 配置缺失重新检查离线打包工程里的配置即可。4.4 安全与合规红线中继攻击、密钥泄露与上线评审最后这部分我把它放在“避坑指南”里是因为很多开发团队在功能实现后就把 NFC 项目当成功了却忽视了安全和合规层面的风险等出了事再补救成本极高。先说行业里常被讨论的“中继攻击”。NFC 的本质是短距离无线通信正常工作时读卡器和卡片的距离只能有十几厘米这本身就是一道物理防线。中继攻击的思路是把读卡器的射频信号远程转发给另一台设备上的卡片相当于把物理距离拉长了攻击者可以在用户不知情的情况下触发刷卡或支付。这个议题提醒我们任何仅依赖“卡片靠近”作为唯一认证因素的系统都有被中继的风险。防御方向是引入动态挑战响应机制门禁读卡器或支付终端在每次交易时生成随机挑战值卡片或手机端必须用密钥体系计算出正确的响应只是简单转发信号无法通过校验。另外在风控层面可以对刷卡频率、刷卡位置、门禁记录做异常检测。再回到钥匙本身。前面说了 Mifare Classic 的密钥体系在多年以前就被公开分析过如果你发现你负责的门禁系统还在用出厂默认密钥或者整栋楼所有卡都共用一个密钥这属于非常严重的安全隐患。作为技术人员项目上线前至少要做一次结构性的安全评审密钥如何生成、如何分发、如何轮换读卡数据在传输过程中是否加密服务端是否有针对重复刷卡、越权读卡的风控策略这些问题的答案应该写进技术文档里而不是等事后补救。合规层面涉及门禁和资金的功能尤其是校园、园区、社区这类面向大量用户的场景一定要在需求阶段就和安全团队、法务对齐。不要在 App 里提供任何批量写卡、修改卡内余额、复制他人卡号的能力即便技术上可行这也是不能碰的红线。我们最终在 App 里只暴露了“读 UID 服务端校验”能力扇区读写全部收敛在发卡器设备端设备本身也需要管理员权限登录才能操作最大程度减少被滥用的面。如果你问我做这个项目最大的心得是什么我的回答是NFC 开发的难点从来不在“能不能读到卡”而在于读懂平台边界、理解卡片协议、尊重安全底线。uni-app 可以做很好的跨端业务层但底层的 NFC 能力必须拿出足够的耐心去啃原生文档。把这个心态放正再配合上面这些经验你的门禁也好、一卡通也好踩坑数量至少能减掉大半。
返回列表