微信小程序NFC实战:MifareClassic M1卡认证与数据读写全流程解析

月半小野猫

1. 微信小程序NFC开发基础

最近在开发智能门禁系统时,我发现很多开发者对微信小程序的NFC功能使用存在不少困惑。特别是针对MifareClassic M1卡的操作,网上资料比较零散。今天我就把自己在实际项目中积累的经验完整分享出来,手把手带你搞定M1卡的认证与数据读写。

微信小程序的NFC API其实封装得相当友好,但有几个关键点需要注意:首先,你的手机必须支持NFC功能;其次,系统NFC开关需要打开;最后,小程序必须获得用户授权才能使用NFC。我在第一次开发时就因为没注意这些基础条件,调试了半天才发现是手机NFC没开启。

MifareClassic M1卡是市面上最常见的IC卡之一,广泛用于门禁、公交卡等场景。它的存储结构就像一栋16层的公寓楼,每层有4个房间(块),每个房间能存放16字节数据。特别要注意的是每层的最后一个房间(块3)存放着重要的钥匙和门禁规则 - 这就是我们后面要重点操作的密钥块。

2. M1卡存储结构详解

2.1 扇区与块的组织方式

M1卡的存储结构可以用"小区-楼栋-房间"来形象理解。整个小区有16栋楼(扇区),每栋楼有4个房间(块),每个房间能放16个字节的数据。其中:

  • 块0-2:普通住户房间,存放用户数据
  • 块3:物业办公室,存放两把钥匙(Key A和Key B)和门禁规则(访问控制位)

实际开发中,我们最常操作的是块0-2的用户数据区。比如在会员卡系统中,块0可以存会员ID,块1存积分,块2存有效期。但要注意,每个扇区的块3是特殊的,它决定了这个扇区的安全规则。

2.2 访问控制位的安全机制

块3的6-9字节存放的访问控制位就像小区的门禁规则,决定了:

  • 哪些钥匙能开哪些门(Key A/B的权限)
  • 能不能修改门禁规则本身
  • 数据块是可读可写还是只读

举个例子,如果控制位设置成Key A只能读不能写,那么即使用户知道Key A,也无法修改数据。我在开发门禁系统时就遇到过,客户提供的测试卡控制位设置得非常严格,导致写入总是失败,后来才发现需要同时提供Key A和Key B才能完成完整操作。

3. 小程序NFC环境搭建

3.1 初始化NFC适配器

在微信小程序中使用NFC功能,首先要初始化适配器。这里有个坑要注意:必须确保用户已经授权NFC权限,否则会静默失败。我建议在onLoad生命周期里就初始化:

javascript复制let adapter = null;
Page({
  onLoad() {
    // 检查NFC支持情况
    if (!wx.getNFCAdapter) {
      this.showToast('当前版本不支持NFC功能');
      return;
    }
    
    adapter = wx.getNFCAdapter();
    this.startDiscovery();
  },
  
  startDiscovery() {
    adapter.startDiscovery({
      success: () => {
        console.log('NFC适配器启动成功');
        this.registerEvents();
      },
      fail: (err) => {
        let msg = 'NFC启动失败';
        if (err.errCode === 13000) msg = '设备不支持NFC';
        else if (err.errCode === 13001) msg = '请打开系统NFC开关';
        this.showToast(msg);
      }
    });
  }
})

3.2 处理卡片发现事件

当NFC适配器检测到卡片时,会触发onDiscovered事件。这里需要判断卡片类型,我们只处理MifareClassic类型的卡片:

javascript复制registerEvents() {
  adapter.onDiscovered((res) => {
    if (res.techs.includes('MIFARE Classic')) {
      this.handleMifareCard(res);
    } else {
      this.showToast('请使用M1卡');
    }
  });
}

handleMifareCard(res) {
  // 获取卡片UID
  const uid = res.id;
  console.log('检测到M1卡,UID:', uid);
  // 后续认证操作...
}

4. M1卡认证流程实战

4.1 Key A与Key B的选择策略

M1卡每个扇区都有两套密钥:Key A和Key B。实际开发中会遇到三种情况:

  1. 只需要Key A认证(最常见)
  2. 只需要Key B认证
  3. 需要双重认证(安全性要求高的场景)

我建议在代码中同时支持两种密钥,通过参数控制使用哪种:

javascript复制function authenticateSector(sector, keyType, key) {
  const mifare = adapter.getMifareClassic();
  const cmd = new Int8Array(12);
  
  // 0x60表示Key A,0x61表示Key B
  cmd[0] = keyType === 'A' ? 0x60 : 0x61;
  cmd[1] = sector * 4;  // 计算块地址
  
  // 填充密钥
  for (let i = 0; i < 6; i++) {
    const byteStr = key.substr(i*2, 2);
    cmd[i+6] = parseInt(byteStr, 16);
  }
  
  return new Promise((resolve, reject) => {
    mifare.transceive({
      data: cmd.buffer,
      success: () => resolve(),
      fail: (err) => reject(err)
    });
  });
}

4.2 认证失败的常见原因

在实际项目中,认证失败主要有以下几种情况:

  1. 密钥错误(最常见)
  2. 卡片已经离开感应区
  3. 访问控制位设置不允许当前操作
  4. 卡片类型不匹配

建议在代码中加入详细的错误处理:

javascript复制async function readSector(sector, keyA, keyB) {
  try {
    // 先尝试Key A
    await authenticateSector(sector, 'A', keyA);
  } catch (err) {
    console.log('Key A认证失败,尝试Key B');
    try {
      await authenticateSector(sector, 'B', keyB);
    } catch (err2) {
      throw new Error('双重认证均失败,请检查密钥和控制位');
    }
  }
  
  // 认证成功后的读取操作...
}

5. 数据读写操作详解

5.1 读取块数据

认证通过后,就可以读取块数据了。M1卡的读取是以块为单位的,每次读取一个块(16字节):

javascript复制function readBlock(block) {
  const mifare = adapter.getMifareClassic();
  const cmd = new Int8Array(2);
  cmd[0] = 0x30;  // 读取指令
  cmd[1] = block; // 块号
  
  return new Promise((resolve, reject) => {
    mifare.transceive({
      data: cmd.buffer,
      success: (res) => {
        const bytes = new Uint8Array(res.data);
        const hexStr = Array.from(bytes)
          .map(b => b.toString(16).padStart(2, '0'))
          .join(':');
        resolve(hexStr);
      },
      fail: reject
    });
  });
}

5.2 写入数据注意事项

写入操作比读取更复杂,需要注意:

  1. 必须先认证通过
  2. 不能直接写块3(控制块)
  3. 某些块可能是只读的
  4. 写入值必须为16字节

这是我封装的安全写入方法:

javascript复制function writeBlock(block, data) {
  if (block % 4 === 3) {
    throw new Error('不能直接写控制块');
  }
  
  if (data.length !== 16) {
    throw new Error('数据长度必须为16字节');
  }
  
  const mifare = adapter.getMifareClassic();
  const cmd = new Int8Array(18);
  cmd[0] = 0xA0;  // 写入指令
  cmd[1] = block; // 块号
  
  // 填充数据
  for (let i = 0; i < 16; i++) {
    cmd[i+2] = data[i];
  }
  
  return new Promise((resolve, reject) => {
    mifare.transceive({
      data: cmd.buffer,
      success: resolve,
      fail: reject
    });
  });
}

6. 实战案例:会员卡系统

6.1 数据结构设计

假设我们要开发一个会员卡系统,可以这样设计数据结构:

  • 扇区0 块0:卡号(8字节) + 手机号后8位
  • 扇区0 块1:积分(4字节) + 注册时间(8字节)
  • 扇区0 块2:有效期(8字节) + 保留字段

对应的读写方法如下:

javascript复制async function readMemberCard(keyA, keyB) {
  // 认证扇区0
  await authenticateSector(0, 'A', keyA);
  
  // 读取三个块
  const [block0, block1, block2] = await Promise.all([
    readBlock(0),
    readBlock(1),
    readBlock(2)
  ]);
  
  // 解析数据
  return {
    cardNo: block0.substr(0, 16).replace(/:/g, ''),
    phone: block0.substr(17, 16).replace(/:/g, ''),
    points: parseInt(block1.substr(0, 8).replace(/:/g, ''), 16),
    registerTime: new Date(parseInt(block1.substr(9, 16).replace(/:/g, ''), 16)),
    expireDate: new Date(parseInt(block2.substr(0, 16).replace(/:/g, ''), 16))
  };
}

6.2 完整工作流程

在实际项目中,完整的NFC操作流程应该是这样的:

  1. 初始化NFC适配器
  2. 等待卡片靠近
  3. 检测卡片类型
  4. 选择目标扇区进行认证
  5. 读取或写入数据
  6. 处理完成关闭连接

这里有个重要细节:每次操作完成后,应该调用mifare.close()释放资源,否则下次操作可能会失败。我在实际开发中就遇到过连续操作时的资源占用问题,后来增加了完善的资源释放逻辑:

javascript复制async function operateCard() {
  const mifare = adapter.getMifareClassic();
  try {
    // 各种NFC操作...
  } catch (err) {
    console.error('操作失败', err);
  } finally {
    mifare.close();
    adapter.stopDiscovery();
  }
}

7. 常见问题与调试技巧

7.1 高频错误排查

在开发过程中,我总结了一些常见错误和解决方法:

  1. NFC适配器启动失败

    • 检查手机是否支持NFC
    • 检查系统NFC开关是否打开
    • 检查小程序是否获得NFC权限
  2. 认证失败

    • 确认使用的密钥是否正确
    • 检查访问控制位设置
    • 确认卡片类型是MifareClassic
  3. 数据读写异常

    • 检查是否先完成了认证
    • 确认操作的块不是控制块
    • 检查数据长度是否符合要求

7.2 调试建议

为了更方便调试NFC功能,我建议:

  1. 在开发阶段打印完整的通信日志
  2. 使用已知数据的测试卡进行验证
  3. 准备多组测试密钥应对不同情况
  4. 使用try-catch包裹所有NFC操作

这是我常用的调试代码片段:

javascript复制function debugHex(buffer, prefix = '') {
  const view = new Uint8Array(buffer);
  const hex = Array.from(view)
    .map(b => b.toString(16).padStart(2, '0'))
    .join(':');
  console.log(prefix, hex);
  return hex;
}

// 在transceive调用前后添加调试信息
mifare.transceive({
  data: cmd.buffer,
  success: (res) => {
    debugHex(res.data, '接收数据:');
  },
  fail: (err) => {
    console.error('操作失败', err);
  }
});

8. 性能优化与安全建议

8.1 操作速度优化

NFC操作有几个耗时点需要注意:

  1. 卡片发现和连接(约200-300ms)
  2. 认证过程(约100ms)
  3. 数据读写(每个块约50ms)

对于需要频繁操作的场景,我有几个优化建议:

  • 批量读取多个块数据(使用Promise.all)
  • 缓存已经认证的扇区信息
  • 减少不必要的连接断开

8.2 安全注意事项

虽然M1卡广泛应用,但它的安全性已经不如现代芯片。在重要场景使用时要注意:

  1. 不要使用默认密钥(如全F或全0)
  2. 定期更换密钥
  3. 对敏感数据加密存储
  4. 考虑使用更安全的芯片(如Mifare DESFire)

在最近的一个门禁系统项目中,我们就遇到了使用默认密钥的安全隐患,后来通过以下措施增强了安全性:

javascript复制// 使用动态密钥派生算法
function deriveKey(baseKey, uid) {
  // 基于UID和主密钥生成扇区专属密钥
  // 实际项目应该使用更安全的算法
  const mixed = baseKey + uid.substr(0, 8);
  return crypto.createHash('md5').update(mixed).digest('hex').substr(0, 12);
}

开发微信小程序的NFC功能确实需要踩不少坑,特别是M1卡的各种特殊情况处理。但一旦掌握了核心流程,就能实现各种有趣的物联网应用。我在实际项目中还遇到过更复杂的情况,比如多扇区操作、密钥滚动更新等,这些高级话题以后有机会再分享。如果你在开发过程中遇到特别棘手的问题,不妨检查下访问控制位的设置,这往往是问题的关键所在。

内容推荐

CentOS 7/8 图形化部署Wireshark:从零搭建网络分析环境
本文详细介绍了在CentOS 7/8系统上图形化部署Wireshark的完整流程,从搭建桌面环境到解决常见依赖问题,再到安装和配置Wireshark图形化界面。文章还提供了首次抓包实战指南和进阶配置技巧,帮助用户快速掌握这一强大的网络分析工具,适用于网络故障排查、安全分析和协议学习等场景。
避坑指南:RK3566 HDMI输入调试中,那些驱动和应用层容易踩的‘坑’(以拔插检测为例)
本文深入探讨了RK3566平台HDMI输入调试中的常见问题与解决方案,重点分析了驱动层和应用层的技术难点。通过实战案例,详细解析了拔插检测、分辨率切换等关键功能的调试方法,并提供了DTS配置、中断处理和应用层适配的专业指导,帮助开发者高效避开HDMIIN调试中的典型陷阱。
当你的NC被Ban了怎么办?5种不依赖Netcat的Linux反弹Shell奇技淫巧
本文详细介绍了5种在Linux系统中无需Netcat即可实现反弹Shell的高阶技巧,包括Bash内置TCP连接、Python多版本兼容方案、系统工具链组合技等。特别针对Netcat被禁用的情况,提供了base64编码绕过等实用方法,帮助渗透测试人员突破工具限制。
别再死记公式了!聊聊数学建模中那些‘活’的概率模型:从随机库存到人口预测
本文探讨了数学建模中概率模型的核心思想与应用实践,从随机库存到人口预测等多个领域展示了其强大的分析能力。通过实例解析和统一框架,帮助读者理解如何在不确性中寻找最优决策,提升数学建模的实际应用价值。
从零到一:手把手教你实现电机电流环PID控制
本文详细介绍了从零开始实现电机电流环PID控制的完整流程,包括硬件电路搭建、PID算法代码实现及参数整定技巧。通过实用的例程和调试方法,帮助初学者快速掌握电流环控制的核心技术,解决响应速度、稳定性和抗干扰等关键问题。
从功耗与成本出发:如何为你的Zynq UltraScale+项目选择最优电源方案(0.72V vs 0.85V实战分析)
本文深入分析了Xilinx Zynq UltraScale+平台在0.72V与0.85V两种电源模式下的系统级权衡,包括性能、功耗、成本及PCB设计影响。通过实测数据与工程案例,为FPGA电源设计提供决策框架,帮助开发者在不同应用场景下选择最优电源方案。
别再死记硬背W底和头肩底了!用Python+TA-Lib实战量化交易中的K线形态识别
本文详细介绍了如何利用Python和TA-Lib库实现量化交易中的K线形态识别,特别是W底和头肩底形态的自动化检测。通过实战代码示例,展示了从环境搭建、数据准备到形态识别策略开发和回测的全流程,帮助交易者提升技术分析效率和准确性。
实战解析:前端调用百度云OAuth接口时CORS跨域报错与代理服务器解决方案
本文详细解析前端调用百度云OAuth接口时遇到的CORS跨域问题,并提供代理服务器解决方案。通过分析报错本质、解释跨域触发原因,并给出uni-app中的具体配置示例,帮助开发者有效解决CORS限制,实现安全高效的API调用。
M1 Mac用户看过来:不装VirtualBox,用PD虚拟机也能跑eNSP的保姆级教程
本文为M1/M2 Mac用户提供了一套无需VirtualBox,通过Parallels Desktop虚拟机流畅运行华为eNSP的完整教程。详细介绍了ARM版Windows镜像选择、Parallels Desktop专业版配置、Npcap替代WinPcap的深度配置等关键步骤,帮助网络工程师在ARM架构上实现高效网络仿真。
保姆级教程:用RK3588的NPU跑通你的第一个AI模型(从环境搭建到推理部署)
本文提供了一份详细的RK3588 NPU开发教程,涵盖从环境搭建到模型推理部署的全流程。重点介绍了RK3588芯片的NPU开发环境配置、模型转换技巧、开发板部署优化以及常见问题排查方法,帮助开发者高效利用6TOPS算力实现AI模型部署。
【Python第三方库】tqdm——从基础到实战的深度应用指南
本文深入探讨Python第三方库tqdm的基础使用与高级技巧,帮助开发者高效实现进度条功能。从安装配置到自定义样式、多进度条并行,再到与Pandas、机器学习及爬虫开发的实战结合,全面展示tqdm在数据处理和任务监控中的强大应用。
别再死记硬背公式了!用PyTorch代码实战搞懂5种卷积(含转置/空洞/深度可分离)
本文通过PyTorch代码实战详细解析了5种卷积操作,包括常规卷积、转置卷积、膨胀卷积、分组卷积和深度可分离卷积。从公式推导到实际应用,帮助开发者深入理解每种卷积的尺寸变化、参数计算及适用场景,特别适合需要优化模型性能的AI工程师和研究人员。
【Java实战】Hutool TreeUtil进阶:自定义排序与动态字段映射的树形结构构建
本文深入探讨了Hutool TreeUtil在Java项目中的进阶应用,重点解析了如何实现自定义排序与动态字段映射的树形结构构建。通过电商后台菜单管理案例,详细展示了突破weight字段限制、多级排序优化、动态字段映射等实用技巧,帮助开发者高效处理复杂业务场景下的树形数据。
第八章:MATLAB结构体进阶:从数据封装到工程实践
本文深入探讨MATLAB结构体在工程实践中的高级应用,从数据封装到性能优化。通过实际案例展示如何利用struct处理多源异构数据,实现高效批量操作与可视化,并分享结构体数组的调试技巧与内存管理策略,帮助工程师提升数据处理效率。
从实战演练到深度解析:一场数据安全竞赛的应急响应全记录
本文详细记录了一场数据安全竞赛中的应急响应实战过程,涵盖Windows事件日志分析、进程监控和网络流量分析三大核心技能。通过异常登录行为识别、攻击源定位、提权过程分析及后门程序检测,展示了从暴力破解到数据窃取的完整攻击链还原方法,为安全从业者提供实用技巧和实战经验。
[实战指南] 基于STM32F103C8T6与MCP4725的I2C DAC扩展方案
本文详细介绍了基于STM32F103C8T6与MCP4725的I2C DAC扩展方案,包括硬件连接、电路设计要点和软件驱动开发。通过实战案例和源码解析,帮助开发者快速实现高精度模拟信号输出,适用于电机控制、音频生成等场景。
基于Docker Macvlan实现OpenWrt旁路由与宿主机双向通信及网关配置
本文详细介绍了如何利用Docker Macvlan网络模式实现OpenWrt旁路由与宿主机的双向通信及网关配置。通过创建Macvlan网络、部署OpenWrt容器并配置宿主机虚拟接口,解决了传统Docker网络隔离导致的通信问题,显著提升网络性能与互通性。文章包含实战步骤、IP规划建议及常见问题排查指南,适合需要优化家庭网络或开发环境的用户。
AD16 PCB设计效率跃迁:深度解析五大核心偏好设置
本文深度解析AD16 PCB设计的五大核心偏好设置,包括PCB Editor、Interactive Routing和Board Insight Display等关键配置,帮助工程师显著提升设计效率。通过优化铺铜自动更新、智能走线、视图显示等设置,可减少40%以上的重复操作时间,特别适用于4层以上复杂板卡设计。
移动机器人激光SLAM导航(一):传感器融合与运动模型解析
本文深入解析移动机器人激光SLAM导航中的传感器融合与运动模型,重点探讨激光雷达、IMU和轮式里程计的多传感器数据融合技术,以及卡尔曼滤波等核心算法在SLAM系统中的应用实践,为移动机器人导航提供理论基础和工程经验。
cwRsync实战:从零搭建Windows高效文件同步服务
本文详细介绍了如何在Windows环境下使用cwRsync搭建高效文件同步服务。从安装配置到实战技巧,涵盖增量同步、权限设置、自动化方案等核心内容,帮助用户解决跨平台文件同步难题,提升工作效率。特别适合需要频繁同步文件的运维人员和开发团队。
已经到底了哦
精选内容
热门内容
最新内容
CUDA 12.1与PyTorch 2.1.0环境搭建:从依赖配置到手动安装的完整指南
本文详细介绍了在Linux系统上搭建CUDA 12.1与PyTorch 2.1.0环境的完整指南,包括系统配置、CUDA安装、cuDNN加速库配置以及PyTorch手动安装步骤。通过清晰的命令和实用技巧,帮助开发者高效完成环境搭建,确保深度学习任务能够顺利运行。
头哥实践平台之MapReduce数据处理实战
本文详细介绍了在头哥实践平台上进行MapReduce数据处理实战的全过程,包括Hadoop环境搭建、学生成绩分析、文件合并去重以及数据关联分析等核心案例。通过具体代码示例和步骤说明,帮助读者快速掌握MapReduce编程技巧,提升大数据处理能力。
从UVM实战看Virtual Interface:老司机教你如何优雅地配置和传递虚接口(附避坑指南)
本文深入探讨了SystemVerilog中virtual interface在UVM验证框架下的工程化实践,详细解析了虚接口的配置、传递策略及常见问题解决方案。通过实际代码示例和架构设计建议,帮助验证工程师优雅地管理虚接口,规避空指针、信号竞争等典型陷阱,提升验证效率与可靠性。
HC32F003串口通信避坑指南:从19200到115200,如何稳定配置UART1(附源码)
本文深入解析HC32F003串口通信的稳定性优化方案,从硬件设计、时钟配置到波特率精准生成技术,提供了一套经过量产验证的UART1稳定通信方案。特别针对19200到115200等高波特率下的数据错乱、丢包问题,分享了中断处理、DMA传输优化等实战技巧,并附完整源码示例。
Ubuntu 22.04 下 VASP 5.4.4 保姆级编译指南:从依赖库到并行测试,一次搞定
本文提供Ubuntu 22.04系统下VASP 5.4.4的完整编译指南,涵盖从依赖库安装到并行测试的全流程。详细讲解环境配置、数学库编译优化、VASP源码编译及性能调优技巧,帮助科研人员高效完成安装并解决常见问题,特别适合计算材料学领域的研究者。
【三维重建】从破损到完美:使用fTetWild实现任意网格的流形水密化实战
本文详细介绍了使用fTetWild工具实现三维网格流形水密化的实战方法。针对3D扫描模型常见的孔洞、自相交和非流形结构问题,fTetWild通过智能填充和自适应优化算法,能够高效生成符合数学要求的流形网格。文章提供了不同场景下的参数配置指南和质量验证技巧,帮助用户快速解决三维重建中的网格修复难题。
从数据流到点云:Intel RealSense Viewer 核心功能实战解析
本文深入解析Intel RealSense Viewer的核心功能,从数据流配置到3D点云交互,帮助开发者高效利用这款3D视觉工具。通过实战案例展示如何优化相机设置、增强深度可视化效果,并分享多场景应用技巧,提升工业测量、机器人导航等领域的开发效率。
编译器架构演进:从GCC的“大一统”到LLVM的“模块化”革命
本文探讨了编译器架构从GCC的'大一统'到LLVM的'模块化'革命演进历程。GCC作为传统编译器代表,其紧密耦合的架构面临维护困难和扩展性差等问题;而LLVM通过引入统一的中间表示(LLVM IR),实现了前后端解耦和优化过程统一,显著提升了编译效率和开发者体验。文章对比了两者在编译速度、内存占用等方面的差异,并分析了模块化架构带来的技术优势与未来发展方向。
CVPR 2020冷门神技:用图像分割的思路‘调教’GAN,让你的生成结果告别‘塑料感’
本文探讨了CVPR 2020上提出的创新方法,通过将U-Net架构引入GAN的判别器设计,显著提升了生成图像的视觉真实感。该方法利用U-Net的像素级反馈机制和CutMix数据增强技术,有效解决了传统GAN生成图像的'塑料感'问题,在FFHQ、CelebA等数据集上实现了显著的FID分数提升。
别再乱用set_multicycle_path了!一个真实案例讲透SDC中的多周期约束(含-start/-end选项详解)
本文深入解析SDC中`set_multicycle_path`命令的正确使用方法,通过真实案例详细讲解多周期路径约束的本质,特别是`-start`和`-end`选项的区别与应用场景。帮助工程师避免常见误用,确保静态时序分析(STA)的准确性,提升芯片设计的可靠性和性能表现。