微信小程序接入NFC,使用HCE模拟主机卡完成NFC刷卡发送消息

Posted 赵小左

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微信小程序接入NFC,使用HCE模拟主机卡完成NFC刷卡发送消息相关的知识,希望对你有一定的参考价值。

NFC相信大家都很熟悉,现实中经常使用的门禁卡,公交卡,地铁卡,饭卡等都是采用NFC功能,那么你知道吗,NFC也可以用微信小程序来实现。使用微信小程序可以读取/写入让手机成为一个刷卡器,也可以使用微信小程序模拟一个主机卡,来刷开门禁/饭卡等等。本章就带大家来一起看看微信小程序的NFC有何不同!

目录

一、什么是NFC

二、NFC可以做什么

三、微信小程序的NFC

四、使用步骤

1.研究API

2.使用方法

一:新项目

二:设置简单页面及对应的js

三:根据上述我们理清的 NFC生命周期顺序来搭建我们的NFC项目。

四:需要开始初始化StartHCE(初始化NFC,将手机初始化为一个主机模拟卡)

五:onHCEMEssage 监听

六:使用wx.sendHCEMessage 发送NFC消息

总结


一、什么是NFC

NFC是一种采用13.56MHz频带的近距离无线通讯技术,虽然通讯距离仅为10cm左右,不过和非接触式IC卡技术一样,我们只需要“触碰一下”即可在不同的电子产品之间交换数据。

与非接触IC卡不同,NFC与非接触式IC卡不同,NFC可进行双向通信。只要是支持NFC的产品和IC卡,就可以读出或写入数据。还可在手机等便携产品间进行通信。数据传输速度不高,有106kbit/秒、212kbit/秒、424kbit/秒以及848kbit/秒四种速度可供选择。

NFC介绍

二、NFC可以做什么

NFC具有“卡模拟”、“读写器模拟”以及“产品间通信(P2P)”三种功能。

1. 卡模拟:举例来说,我们可以用手机来模拟门禁卡,公交卡,饭卡等等。但是要注意的是,它是模拟出来的,也就是说并不实体存在的一个卡。

2. 读写器模拟:指的就是,你可以模拟一个读写器,别人用NFC卡来你这里刷卡,你就可以获取其卡片上的信息。

3. 通讯讲就是数据通信,使用NFC卡在刷的时候都会进行数据交换。用来做一些事情,比如:配置网络,设置信息,传输文件等等。

三、微信小程序的NFC

NFC | 微信开放文档

我们打开文档可以看到,对于NFC,微信给它的名字叫:近场通讯

首先一定要强调的是,因为苹果手机权限的缘故,所以NFC在微信小程序中只支持:android

微信将NFC分为了三部分

  • HCE(基于主机的卡模拟),也就是将安卓手机模拟成实体的智能卡。我们可以通过模拟的智能卡来刷对应的读卡器,给读卡器传递数据。
  • 支持NFC读写,也就是将手机作为读卡器来使用。也就是我们可以将一些实体的NFC卡通过贴在手机上从而实现读取卡内容。
  • NFC标签打开小程序。指的就是我们可以通过NFC卡片触碰手机,快速唤起小程序页面的能力。

1. HCE(基于主机的卡模拟)的使用场景:

  • 使用手机做门禁卡/公交卡/地铁卡
  • 使用手机给读卡器传递数据(配网、登记)

2. NFC读写的使用场景:

  • 使用手机做刷卡器,来获取NFC卡的信息。

3. NFC标签打开小程序的使用场景

  • 设备的快速配网
  • 文件快速传输等快捷控制

官网文档写的也很齐全了,大家可以根据自己的需求选择不同的场景来进行使用。

本章我就选择了HCE(基于主机的卡模拟)场景。

四、使用步骤

1.研究API

wx.stopHCE(Object object) | 微信开放文档

微信官方文档中,卡模拟一共有六个API。

分别为:

wx.stopHCE 关闭NFC模块。

wx.startHCE 初始化NFC

wx.sendHCEMessage 发送NFC消息

wx.onHCEMessage  监听接收NFC设备消息事件。

wx.offHCEMessage  移除接收 NFC 设备消息事件的监听函数

wx.getHCEState 判断当前设备是否支持 HCE 能力。

再次特别要强调的是,NFC仅在Android系统下支持。

2.使用方法

有过开发经验的同学其实比较清楚。

以上API 很明显就是一个生命周期

从开始到销毁。我们该如何去操作这个NFCapi呢?

1. 首先调用 wx.getHCEState, 判断设备是否支持NFC

2. 调用 wx.startHCE(OBJECT) 初始化手机的NFC模块;

3. 初始化完成后,调用 wx.onHCEMessage 监听芯片响应的消息;

4. 点击页面上的“询卡”按钮,调用 wx.sendHCEMessage发送询卡指令;

5. 这时 wx.onHCEMessage(应该可以收到带有uid信息的芯片响应数据;

6. 业务处理。

7.  全部操作完成后之后,调用 wx.stopHCE停止手机的NFC模块

好了,废话不多说,直接进入我们的正式项目。

一:新项目

我们需要利用微信开发者工具,来新建一个我们的项目。APPID大家如果有的话就用自己的,没有的话则使用测试号即可。

二:设置简单页面及对应的js

三:根据上述我们理清的 NFC生命周期顺序来搭建我们的NFC项目。

顺序自然为:页面打开初始化的时候就需要 getHCEState 判断设备是否支持NFC。不支持则需要做兼容。

当返回值的errCode为 0  的时候则一切正常,是支持NFC的。

同时,为了更有效的避免大家判断code,所以我们可以将官网的 getHCEState返回code 声明出来,以便后面使用。

四:需要开始初始化StartHCE(初始化NFC,将手机初始化为一个主机模拟卡)

 wx.startHCE接收一个参数为 aid_list。

官方解释意思为,需要注册到系统AID列表中,AID列表其实就是每个刷卡器的唯一标识,不清楚的就问下你们的安卓或者默认填写:F22222222。与其它API 类似,它也有自己的返回值,默认0为正常成功的。

五:onHCEMEssage 监听

在说这里之前,网上许多朋友碰到的类似,wx.onHCEMessage,不管怎么调都没有返回值?

不知道有没有伙伴仔细看我上面的话,初始化完成后,onHCEMessage其实就要开始监听。至于为什么没有返回值。是因为,wx.onHCEMessage是一个监听。它需要读卡器来响应它,也就是说要给需要读卡器(设备)通过render 指令 给它发消息,这时候它才能拿得到值。

 

它有三个返回值,但是我们只一般取第一个也就是 messageType。

messageType的值 = 1 时,也就是说,读卡器给我们响应了,说它接收到了NFC。这时候我们就可以调用 sendHCEMessage 来给读卡器发送消息了。

messageType的值 = 2 时,就是说,手机已经从读卡器上拿开了。

 所以我们在这里只判断 messageType的值为1. 当等于1 的时候我们就可以开始发消息了。

六:使用wx.sendHCEMessage 发送NFC消息

细心的朋友会发现,发个消息不就是 wx.sendHCEMessage就够了。为啥我的有一大堆的ArrayBuffer 甚至还有comm.

这是因为文档有标注:

wx.sendHCEMessage 的data 必须是一个二进制数据。也就是我们不能将普通的JSON,字符串等数据 传递给它。否则会报错。

那这时候我们需要怎么办呢?

转换!

我在项目中的comm 就是一个封装的 转换文件。内容如下:

comm.js

const formatTime = date => 
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const hour = date.getHours()
  const minute = date.getMinutes()
  const second = date.getSeconds()

  return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')

/**
 * 生成指定长度随机数
 */
function genRandom(n) 
  let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; //生成的随机数的集合  
  let res = [];
  for (let i = 0; i < n; i++) 
    let index = parseInt(Math.random() * (a.length));    //生成一个的随机索引,索引值的范围随数组a的长度而变化  
    res.push(a[index]);
    a.splice(index, 1)  //已选用的数,从数组a中移除, 实现去重复  
  
  return res.join('');
 
/**
    * 字符串转换为时间
    * @param  String src 字符串
    */
function strToDate(dateObj)
  dateObj = dateObj.replace(/T/g, ' ').replace(/\\.[\\d]3Z/, '').replace(/(-)/g, '/')
  dateObj = dateObj.slice(0, dateObj.indexOf("."))
  return new Date(dateObj)

function isFunctinMethod(name) 
  if (name != undefined && typeof name === 'function') 
    return true
  
  return false

const formatNumber = n => 
  n = n.toString()
  return n[1] ? n : '0' + n


// ArrayBuffer转16进度字符串
function ab2hex(buffer) 
  var hexArr = Array.prototype.map.call(
    new Uint8Array(buffer),
    function (bit) 
      return ('00' + bit.toString(16)).slice(-2)
    
  )
  return hexArr.join('');


//十六进制字符串转字节数组  
function hex2Bytes(str) 
  var pos = 0;
  var len = str.length;
  if (len % 2 != 0) 
    return null;
  
  len /= 2;
  var hexA = new Array();

  for (var i = 0; i < len; i++) 
    var s = str.substr(pos, 2);
    var v = parseInt(s, 16);
    hexA.push(v);
    pos += 2;
  
  return hexA;

function hex2ArrayBuffer(hex)
  var pos = 0;
  var len = hex.length;
  if (len % 2 != 0) 
    return null;
  
  len /= 2;
  var buffer = new ArrayBuffer(len)
  var dataview=new DataView(buffer)
  for (var i = 0; i < len; i++) 
    var s = hex.substr(pos, 2);
    var v = parseInt(s, 16);
    dataview.setInt16(i,v)
    pos += 2;
  

  return buffer

/**
 * string转16进制
 */
function stringToHex(str) 
  var val = "";
  for (var i = 0; i < str.length; i++) 
    if (val == "")
      val = str.charCodeAt(i).toString(16);
    else
      val += str.charCodeAt(i).toString(16);
  
  return val;

/**
 * 16进制转string
 */
function hexCharCodeToStr(hexCharCodeStr) 
    var trimedStr = hexCharCodeStr.trim();
    var rawStr =
      trimedStr.substr(0, 2).toLowerCase() === "0x"
        ?
        trimedStr.substr(2)
        :
        trimedStr;
    var len = rawStr.length;
    if (len % 2 !== 0) 
        alert("Illegal Format ASCII Code!");
        return "";
    
    var curCharCode;
    var resultStr = [];
    for (var i = 0; i < len; i = i + 2) 
        curCharCode = parseInt(rawStr.substr(i, 2), 16); // ASCII Code Value
        resultStr.push(String.fromCharCode(curCharCode));
    
    return resultStr.join("");


function pad(num, n) 
  var len = num.toString().length;
  while (len < n) 
    num = "0" + num;
    len++;
  
  return num;


function strToHexCharCode(str) 
    if (str === "")
        return "";
    var hexCharCode = [];
    hexCharCode.push("0x");
    for (var i = 0; i < str.length; i++) 
        hexCharCode.push((str.charCodeAt(i)).toString(16));
    
    return hexCharCode.join("");

/**
 * string转byte数组
 */
function stringToByteArray(str) 
  var bytes = new Array();
  var len, c;
  len = str.length;
  for (var i = 0; i < len; i++) 
    c = str.charCodeAt(i);
    if (c >= 0x010000 && c <= 0x10FFFF) 
      bytes.push(((c >> 18) & 0x07) | 0xF0);
      bytes.push(((c >> 12) & 0x3F) | 0x80);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
     else if (c >= 0x000800 && c <= 0x00FFFF) 
      bytes.push(((c >> 12) & 0x0F) | 0xE0);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
     else if (c >= 0x000080 && c <= 0x0007FF) 
      bytes.push(((c >> 6) & 0x1F) | 0xC0);
      bytes.push((c & 0x3F) | 0x80);
     else 
      bytes.push(c & 0xFF);
    
  
  return bytes;

/**
 * byte数组转string
 */
function byteToString(bytearr) 
  if (typeof arr === 'string') 
    return arr;
  
  var str = '',
    _arr = arr;
  for (var i = 0; i < _arr.length; i++) 
    var one = _arr[i].toString(2),
      v = one.match(/^1+?(?=0)/);
    if (v && one.length == 8) 
      var bytesLength = v[0].length;
      var store = _arr[i].toString(2).slice(7 - bytesLength);
      for (var st = 1; st < bytesLength; st++) 
        store += _arr[st + i].toString(2).slice(2);
      
      str += String.fromCharCode(parseInt(store, 2));
      i += bytesLength - 1;
     else 
      str += String.fromCharCode(_arr[i]);
    
  
  return str;

/**
 * 二进制转10
 */
function bariny2Ten(byte)

  return parseInt(byte, 2)

function bariny2Hex(a)
  return parseInt(a, 16)

/**
 * 10/16进制转2进制
 */
function ten2Bariny(ten)
  
  return ten.toString(2)

function str2Hex(str)
  return parseInt(str, 10).toString(16)

/**
 * 16进制转2进制
 */
function hex2bariny(hex)
  return parseInt(hex, 16).toString(2)

module.exports = 
  formatTime: formatTime,
  isFunctinMethod: isFunctinMethod,
  ab2hex: ab2hex,
  hex2Bytes: hex2Bytes,
  stringToByteArray: stringToByteArray,
  byteToString: byteToString,
  hex2ArrayBuffer: hex2ArrayBuffer,
  bariny2Ten: bariny2Ten,
  bariny2Hex: bariny2Hex,
  ten2Bariny: ten2Bariny,
  str2Hex: str2Hex,
  hex2bariny: hex2bariny,
  genRandom: genRandom,
  stringToHex: stringToHex,
  hexToString: hexCharCodeToStr,
  pad: pad

 它里面包括了 string 转 字节数组等常用转换方法。

至于 ArrayBuffer  以及 DataView 等方法,相信各位也都知道,就不在这里做详解。不了解的可以在CSDN上搜索学习。

由于我在这里给后台需要传递的是一个JSON,所以我就将JSON转换为字符串。然后再将字符串转换为字节数组,传递给了消息。

comm就是 上述封装的转化文件。comm.js 

stringToByteArray 方法就是 comm.js 文件中封装的 string转 字节数组的方法。

当我们完成这一切后使用“真机调试”前往对应刷卡器跟前刷卡即可!会发现前端已经成功了,并且读卡器也已经在控制台上打印出了我们传递的数据。

至此,我们的项目就结束了。

总结

以上就是今天分享给大家的微信小程序HCE模拟主机卡的功能。

但是其中不缺乏有一些坑。我列出来供大家参考!

1. wx.onHCEMessage 没有返回值。

这个可能是我见过最多的问题了,其实它没有返回值的原因就是,读卡器没有 返回信息。通俗点讲就是,你手机开启了NFC,当你挨着读卡器的时候,手机不知道你有没有挨上读卡器,所以需要读卡器给你说一句,你挨上我了,可以发消息了。所以这块的处理是需要对应的客户端开发来处理的,小程序端已经结束了。如果客户端开发不会的话,在此处放一个大佬的文章,可以让客户端开发参考下。微信小程序 NFC HCE卡模拟+AndroidNFC读取优必果

2. 我的手机刷卡没反应。是我哪里写错了吗?

如果你是按照上面的步骤同时结合官网文档来进行的,那么就基本没错。如果刷卡没反应,并且这时候客户端已经是处理好了的。那么只有一个可能,手机兼容性不够。你换个安卓手机试试。(PS:我试了 vivo跟华为。vivo 有两个手机不支持。华为目前使用的一款是支持的)、

3. aid_list 是什么,怎么找呢?

其实最简单的就是找你们客户端开发,他们有办法找到你的读卡器的 aid, 默认一般都是F222222222 。如果不是那就找一找你们的客户端开发,让他帮你找一下。

好了,文章就到这里。后面大家若是在开发过程有疑问,欢迎提问、私信我。

NFC开发(一)——HCE基于主机的卡模拟简述

参考技术A

许多提供NFC功能的基于Android的设备已经支持NFC卡模拟。在大多数情况下,该卡由设备中的单独芯片模拟,称为 安全元件(Secure Element) 。无线运营商提供的许多SIM卡还包含安全元件(Secure Element)。

Android 4.4引入了另一种卡模拟方法,它不涉及SE,称为 基于主机的卡模拟 。这允许任何Android应用程序模拟卡并直接与NFC读卡器通话。本文档描述了基于主机的卡仿真(HCE)如何在Android上工作,以及如何使用此技术开发模拟NFC卡的应用程序。

当使用安全元件(Secure Element)提供NFC卡模拟时,将通过Android应用程序将要模拟的卡提供到设备上的安全元件(Secure Element)中。然后,当用户通过NFC终端握住设备时,设备中的NFC控制器将来自读卡器(NFC Reader)的所有数据直接路由到安全元件(Secure Element)。图1说明了这个概念。

安全元件(Secure Element)本身执行与NFC终端的通信,并且完全不涉及Android应用。交易完成后,Android应用程序可以直接查询SE的交易状态并通知用户。

当使用基于主机的卡仿真来仿真NFC卡时,数据将被路由到直接运行Android应用程序的主机CPU,而不是将NFC协议帧路由到SE。图2展示了基于主机的卡仿真如何工作。

NFC标准提供对许多不同协议的支持,并且可以模拟不同类型的卡。

Android 4.4支持当今市场上常见的几种协议。许多现有的非接触式卡已经基于这些协议,例如非接触式支付卡。这些协议也得到了当今市场上众多NFC读卡器的支持,其中包括Android NFC设备可以自己作为读卡器(请参见 IsoDep 课程)。这使您可以仅使用基于Android的设备在HCE周围构建和部署端到端NFC解决方案。

具体而言,Android 4.4支持基于NFC-Forum ISO-DEP规范(基于ISO / IEC 14443-4)的仿真卡,并处理ISO / IEC 7816-4规范中定义的应用协议数据单元(APDU)。Android只强制在Nfc-A(ISO / IEC 14443-3 Type A)技术之上模拟ISO-DEP。支持Nfc-B(ISO / IEC 14443-4 Type B)技术是可选的。所有这些规格的分层如图3所示。

Android中的HCE体系结构基于Android Service 组件(称为“HCE服务”)。服务的一个关键优势是它可以在没有任何用户界面的情况下在后台运行。这对于许多HCE应用程序来说非常合适,例如会员卡或公交卡,用户不需要启动应用程序即可使用它。相反,通过NFC读卡器轻敲设备将启动正确的服务(如果尚未运行)并在后台执行该事务。当然,如果有意义的话,您可以自由地从您的服务中启动额外的UI(例如用户通知)。

当用户将设备连接到NFC读取器时,Android系统需要知道NFC读取器实际想要与哪个HCE服务通话。这就是ISO / IEC 7816-4规范的出处:它定义了一种选择应用程序的方式,以应用程序ID(AID)为中心。一个AID最多由16个字节组成。如果您正在模拟现有NFC读卡器基础架构的卡片,那么这些读卡器所寻找的AID通常是众所周知的并且是公开注册的(例如Visa和MasterCard等支付网络的AID)。

如果您想为自己的应用程序部署新的读卡器基础结构,则需要注册您自己的AID。AID的注册程序在ISO / IEC 7816-5规范中定义。如果您要为Android部署HCE应用程序,Google建议按照7816-5注册AID,因为它可以避免与其他应用程序发生冲突。

在某些情况下,HCE服务可能需要注册多个AID才能实现某个应用程序,并且需要确保它是所有这些AID的默认处理程序(而不是组中的某些AID转到其他服务) 。

一个AID组是应该被OS视为一起归属的AID列表。对于AID组中的所有AID,Android会保证以下其中一项:

换句话说,没有中间状态,组中的一些AID可以路由到一个HCE服务,另一些AID可路由到另一个。

每个AID组都可以与一个类别关联。这允许Android按类别将HCE服务组合在一起,并且反过来又允许用户在类别的级别而不是AID级别设置默认值。通常,避免在应用程序的任何面向用户的部分提及AID:它们对普通用户没有任何意义。

Android 4.4支持两种类别: CATEGORY_PAYMENT (涵盖行业标准支付应用程序)和 CATEGORY_OTHER (对于所有其他HCE应用程序)。

要使用基于主机的卡仿真来模拟NFC卡,您需要创建一个 Service 处理NFC事务的组件。

您的应用程序可以通过检查 FEATURE_NFC_HOST_CARD_EMULATION 功能来检查设备是否支持HCE 。您应该 <uses-feature> 在应用程序清单中使用该标记来声明您的应用程序使用HCE功能,以及该应用程序是否需要运行。

Android 4.4带有一个便利的 Service 类,可以作为实现HCE服务的基础: HostApduService 类。

因此,第一步要扩大 HostApduService 。

HostApduService 声明了两个需要重写和实现的抽象方法。

processCommandApdu() 只要NFC读卡器将应用协议数据单元(APDU)发送到您的服务,就会调用它。APDU也在ISO / IEC 7816-4规范中定义。APDU是在NFC读卡器和您的HCE服务之间交换的应用级数据包。该应用级协议是半双工的:NFC读卡器会向您发送命令APDU,并等待您发送响应APDU作为回报。

如前所述,Android使用AID来确定读者想要与哪个HCE服务交谈。通常,NFC读卡器向您的设备发送的第一个APDU是“SELECT AID”APDU; 这个APDU包含读卡器想与之交谈的AID。Android从APDU中提取AID,将其解析为HCE服务,然后将该APDU转发给已解析的服务。

您可以通过返回响应APDU的字节来发送响应APDU [processCommandApdu()]( https://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#processCommandApdu(byte[] , android.os.Bundle))。请注意,此方法将在应用程序的主线程中调用,该线程不应被阻止。所以如果你不能立即计算并返回一个响应APDU,那么返回null。然后,您可以在另一个线程上完成必要的工作,并 sendResponseApdu() 在完成后使用 HostApduService 该类中定义的方法发送响应。

Android会继续将新的APDU从读取器转发到您的服务,直到:

在这两种情况下,你的类的 onDeactivated() 实现都是通过一个参数来调用的,这个参数指出了两者中的哪一个发生了。

如果您正在使用现有的读卡器基础架构,则需要实现读卡器在您的HCE服务中期望的现有应用程序级协议。

如果您正在部署您控制的新读卡器基础架构,则可以定义自己的协议和APDU序列。通常,尝试限制APDU数量和需要交换的数据大小:这样可以确保用户只需将设备通过NFC读取器持续一段时间即可。合理的上限约为1KB的数据,通常可以在 300ms 内交换。

您的服务必须像往常一样在清单中声明,但还必须在服务声明中添加一些附加件。

首先,为了告诉平台它是一个实现 HostApduService 接口的HCE服务 ,你的服务声明必须包含一个 SERVICE_INTERFACE 动作的 Intent Filter 。

另外,为了告知平台哪个AIDs组被这个服务请求,一个 SERVICE_META_DATA <meta-data> 标签必须包含在服务的声明中,指向一个XML资源和关于HCE服务的附加信息。

最后,您必须将该 android:exported 属性设置为true,并且 "android.permission.BIND_NFC_SERVICE" 在服务声明中要求权限。前者确保服务可以被外部应用程序绑定。后者然后强制只有拥有该 "android.permission.BIND_NFC_SERVICE" 权限的外部应用程序 才能绑定到您的服务。既然 "android.permission.BIND_NFC_SERVICE" 是一个系统权限,这有效地强制只有Android OS可以绑定到你的服务。

这是一个 HostApduService 清单声明的例子:

这个元数据标签指向一个 apduservice.xml 文件。下面显示了具有包含两个专有AID的单个AID组声明的此类文件的示例:

该 <host-apdu-service> 标签需要包含一个 <android:description> 属性,该属性包含可能在UI中显示的用户友好的服务描述。该 requireDeviceUnlock 属性可用于指定在调用此服务来处理APDU之前必须先解锁设备。

在 <host-apdu-service> 必须包含一个或多个 <aid-group> 标签。每个 <aid-group> 标签都需要:

最后,您的应用程序还需要拥有 NFC 可以注册为HCE服务的 权限。

多个 HostApduService 组件可以安装在单个设备上,并且可以由多个服务注册相同的AID。Android平台根据AID属于哪个类别来解决AID冲突。每个类别可能有不同的冲突解决策略。

例如,对于某些类别(如付款),用户可能能够在Android设置UI中选择默认服务。对于其他类别,策略可能总是要求用户在冲突情况下调用哪个服务。要查询特定类别的冲突解决策略,请参阅 getSelectionModeForCategory() 。

应用程序可以使用[isDefaultServiceForCategory(ComponentName, String)]( https://developer.android.com/reference/android/nfc/cardemulation/CardEmulation.html#isDefaultServiceForCategory(android.content.ComponentName , java.lang.String))API 检查其HCE服务是否是某个类别的默认服务。

如果您的服务不是默认设置,则可以请求将其设置为默认设置。看 ACTION_CHANGE_DEFAULT 。

Android会将AID组为“payment”的类别,声明的HCE服务视为支付应用程序。Android 4.4版本包含一个名为“tap&pay”的top-level设置菜单条目,它列举了所有这些支付应用程序。在此设置菜单中,用户可以选择在点按付款终端时将调用的默认支付应用程序。

为了提供更具视觉吸引力的用户体验,HCE支付应用程序需要为其服务提供额外的resource:所谓的服务标记。

这个asset的大小应该是260x96 dp,并且可以在元数据(meta-data)XML文件中通过添加指向drawable resource android:apduServiceBanner 的 <host-apdu-service> 标签的属性来指定 。一个例子如下所示:

当设备的屏幕关闭时,当前的Android实施将NFC控制器和应用程序处理器完全关闭。因此,当屏幕关闭时,HCE服务将无法工作。

然而,HCE服务可以从锁定屏幕中起作用:这由HCE服务标记中的 android:requireDeviceUnlock 属性控制 <host-apdu-service> 。默认情况下,不需要设备解锁,即使设备被锁定,您的服务也会被调用。

如果您将 android:requireDeviceUnlock HCE服务的属性设置为“true”,Android会提示用户在您靠近NFC读卡器时解锁设备,NFC读卡器会选择已解析为您的服务的AID。解锁后,Android会显示一个对话框,提示用户再次点击以完成交易。这是必要的,因为用户可能已经将设备从NFC读卡器移开以便解锁它。

本部分对于已经部署依赖SE进行卡模拟的应用程序的开发人员很感兴趣。Android的HCE实现旨在与其他实现卡仿真的方法并行工作,包括使用SE。

这种共存基于一种称为“AID路由”的原则:NFC控制器保留一个由(有限)路由规则列表组成的路由表。每个路由规则都包含一个AID和一个目的地。目标可以是主机CPU(Android应用程序正在运行的地方),也可以是连接的SE。

当NFC读卡器发送具有“SELECT AID”的APDU时,NFC控制器解析它并检查AID是否与其路由表中的任何AID匹配。如果匹配,那么APDU和其后的所有APDU将被发送到与AID相关联的目的地,直到收到另一个“SELECT AID” APDU或NFC链路断开。

图4说明了这种架构。

NFC控制器通常还包含APDU的默认路由。在路由表中找不到AID时,将使用默认路由。尽管此设置可能因设备而异,但Android设备需要确保您的应用注册的AID已正确路由到主机。

实现HCE服务或使用SE的Android应用程序不必担心配置路由表 - 这是由Android自动处理的。Android只需要知道哪些AID可以由HCE服务处理,哪些可以由SE处理。基于哪些服务已安装,以及哪些用户已配置为首选服务,路由表会自动配置。

我们已经介绍了如何声明HCE服务的AID。以下部分说明如何为使用SE进行卡模拟的应用程序声明AID。

使用SE进行卡模拟的应用程序可以在其清单中声明所谓的“关闭主机服务”。这种服务的声明几乎与宣布HCE服务相同。以下情况例外:

相应 apduservice.xml 文件注册两个AID 的示例:

该 android:requireDeviceUnlock 属性不适用于脱离主机服务,因为主机CPU不参与事务,因此无法阻止SE在设备锁定时执行事务。

该 android:apduServiceBanner 属性必须用于作为支付应用程序的关闭主机服务,以便作为默认支付应用程序进行选择。

Android本身永远不会启动或绑定到声明为“脱离主机”的服务。这是因为实际交易由SE执行,而不是由Android服务本身执行。服务声明仅允许应用程序注册安全元件(Secure Element)上存在的AID。

HCE体系结构本身提供了一个核心安全性:因为您的服务受到 BIND_NFC_SERVICE 系统权限的保护,所以只有操作系统可以绑定到您的服务并与之通信。这可以确保您收到的任何APDU实际上都是OS从NFC控制器接收到的APDU,并且您发回的任何APDU只会发送到操作系统,而操作系统会直接将APDU转发给NFC控制器。

剩下的核心部分就是您获取应用程序发送给NFC读卡器的数据的位置。这在HCE设计中有意解耦:它不关心数据来自何处,它只是确保将其安全地传送到NFC控制器并传送到NFC读取器。

为了安全地存储和检索您希望从HCE服务发送的数据,例如,您可以依靠Android应用程序沙箱,将应用程序的数据与其他应用程序隔离。有关Android安全性的更多详细信息,请阅读 安全提示 。

这部分内容对于希望了解HCE设备在NFC协议的防冲突和激活阶段使用何种协议参数的开发人员很感兴趣。这允许构建与Android HCE设备兼容的读卡器基础结构。

作为Nfc-A协议激活的一部分,交换多个帧。

在交换的第一部分,HCE设备将呈现其UID; HCE设备应该被假定为具有随机的UID。这意味着在每个抽头中,呈现给读卡器的UID将是随机生成的UID。因此,NFC读卡器不应依赖HCE设备的UID作为身份验证或身份验证的一种形式。

NFC读取器可以随后通过发送SEL_REQ命令来选择HCE设备。HCE设备的SEL_RES响应将至少设置第6位(0x20),表示设备支持ISO-DEP。注意,SEL_RES中的其他位也可以被设置,表示例如对NFC-DEP(p2p)协议的支持。由于可以设置其他位,所以想要与HCE设备交互的读者应该明确检查第6位,并且<stront style="box-sizing: inherit;">不要将完整的SEL_RES与值0x20进行比较。</stront>

Nfc-A协议激活后,NFC读取器启动ISO-DEP协议激活。它发送一个“RATS”(请求选择应答)命令。RATS响应(ATS)完全由NFC控制器生成,不能由HCE服务配置。然而,HCE实现需要满足NFC论坛对ATS响应的要求,因此NFC读卡器可以根据NFC论坛对任何HCE设备的要求设置这些参数。

以下部分提供了有关NFC控制器在HCE设备上提供的ATS响应的各个字节的更多详细信息:

请注意,许多HCE设备可能符合EMVCo联合的支付网络在其“非接触式通信协议”规范中指定的协议要求。尤其是:

如前所述,HCE实现仅支持单个逻辑通道。尝试在不同的逻辑通道上选择应用程序将不适用于HCE设备。

本文 翻译自 谷歌开发者文档,已由本人仔细校对。如有错误,请联系我,以便修改。

以上是关于微信小程序接入NFC,使用HCE模拟主机卡完成NFC刷卡发送消息的主要内容,如果未能解决你的问题,请参考以下文章

nfc-a协议的时序要求

HCE对金融交通通讯智能卡行业的巨大影响 电子虚拟智能卡 Android version4.4 NFC手机

HCE对金融交通通讯智能卡行业的巨大影响 电子虚拟智能卡 Android version4.4 NFC手机

测试 HCE 支付应用程序

微信小程序卡券接入流程,酷客多为你支招

链接:NFC:基于主机的卡模拟