近场通信(NFC)使两个设备在近距离内(通常少于几厘米)进行无线通信成为可能。 NFC 是一项国际标准(ISO/IEC 18092),定义了在 13.56 MHz 工作的紧密耦合设备之间用于简单无线互联的接口和协议。
硬件标准在 [[[NFC-STANDARDS]]] 中定义。
本文档定义了基于 NFC 技术启用若干选定用例的 API。本规范当前的范围是 NDEF。
低级别 I/O 操作(例如 ISO-DEP、NFC-A/B、NFC-F)以及基于主机的卡模拟(HCE)在当前范围内 不受支持。
实现者需要注意,本规范被视为不稳定。 不参与讨论的实现者会发现规范会以不兼容的方式不断变化。希望在规范最终进入候选推荐阶段之前实现本规范的厂商,应订阅 GitHub 上的仓库并参与讨论。
本文档定义了适用于单一产品的符合性标准:实现其所包含接口的 UA(用户代理)。
Web NFC 的用户场景如下:将设备保持在被动供电的 NFC 标签(例如塑料卡或贴纸)近旁,以读取和/或写入数据。
NFC 使用磁感应工作,这意味着读取器(一个有源、有电的设备)会发出微小电荷,从而产生磁场。 该磁场为被动设备供电,被动设备将其转换为电信号以传输数据。因此,当设备处于有效范围内时,会始终进行读取(参见 NFC Analog Specification 和 NFC Digital Protocol,NFC Forum,2006)。 对等连接的工作方式类似,设备会周期性地切换到所谓的发起器模式以扫描目标,然后再切换回目标模式。如果找到目标,则以与标签相同的方式读取数据。
由于 NFC 基于现有的 RFID 标准,许多 NFC 芯片组支持读取 RFID 标签,但其中有些仅由单一厂商支持,并非 NFC 标准的一部分。因此,本文件规定了与 NFC 数据交换格式(NDEF)交互的方法。
使用的扩充巴科斯-诺尔范式(ABNF)表示法在 [[RFC5234]] 中规定。
NFC 表示近场通信,一种在 13.56 MHz 工作的短距离无线技术,使设备在小于 10 cm 的距离内进行通信。NFC 通信协议和数据交换格式基于现有的射频识别(RFID)标准,包括 ISO/IEC 14443 和 FeliCa。 NFC 标准包括 ISO/IEC 18092[5] 以及 NFC Forum 定义的标准。完整列表请参见 NFC Forum 技术规范。
NFC adapter 是底层平台中的软件实体,提供对在给定硬件组件(NFC 芯片)中实现的 NFC 功能的访问。设备可能有多个 NFC 适配器,例如内置的一个,或者通过 USB 连接的一个或多个。
读取消息的方式可能通过专有技术发生,这些技术要求读取器和标签来自相同厂商。它们也可能暴露一个 NDEF 消息。
如当前规范所述,点对点(peer-to-peer)不被支持。
NFC device 是 NFC peer 或 NFC tag。
NDEF 是 NFC Forum 数据交换格式(NFC Forum Data Exchange Format)的缩写,一种在 [[!NFC-NDEF]] 中标准化的轻量二进制消息格式。
NDEF message 封装一个或多个应用定义的 NDEF record。NDEF 消息可以存储在 NFC tag 上或在启用 NFC 的设备之间交换。
术语 NFC content 表示发送到或从 NFC tag 接收的所有字节。在当前 API 中,它与 NDEF message 同义。
NFC 在 NFC Forum 中被标准化,并在 [[NFC-STANDARDS]] 中描述。
NFC Forum 已规定支持五种不同的标签类型以便与 NFC 设备一起操作。操作系统(如 Android)也要求同样的支持。
除此之外,MIFARE Standard 指定了一种在较旧的 MIFARE Standard 之上使 NDEF 工作的方式,实施者可选择性地支持该方式。
关于 NDEF 映射的说明可以在此找到: MIFARE Classic 作为 NFC 类型 MIFARE Classic 标签。
MIFARE Standard 不是 NFC Forum 类型,只能被使用 NXP 硬件的设备读取。对基于 MIFARE Standard 的标签进行读写的支持因此不是命名性支持,但由于其在传统系统中的流行性和使用而被包含在内。
除了 NFC Forum 为 NDEF record 标准化的数据类型外,许多商业产品(例如公交卡、门禁卡)可能基于 MIFARE Standard,其依赖特定的 NFC 芯片(卡和读取器来自相同厂商)才能工作。
NDEF record 是 NDEF message 的一部分。每个记录都是包含数据有效载荷及相关类型信息的二进制结构。此外,它还包含有关数据结构的信息,例如有效载荷大小、数据是否在多个记录之间分片等。
只有前三个字节(图中行)是强制性的。首先是头字节,其次是 TYPE LENGTH field 和 PAYLOAD LENGTH field,二者都可以为零。
| TNF 值 | 描述 |
|---|---|
| 0 | 空记录 |
| 1 | NFC Forum [=well-known type record=] |
| 2 | MIME type record |
| 3 | Absolute-URL record |
| 4 | NFC Forum external type record |
| 5 | Unknown record |
| 6 | Unchanged record |
| 7 | 预留用于将来使用 |
IL field(位 `3`,ID 长度)指示是否存在 ID LENGTH field。如果 IL field 为 `0`,则 ID field 也不存在。
SR field(位 `4`,短记录)指示短记录,其有效载荷长度 <= `255` 字节。普通记录的有效载荷长度可以超过 `255` 字节,最大可达 `4` GB。短记录只使用一个字节指示长度,而普通记录使用 `4` 字节(`2``32``-1` 字节)。
CF field(位 `5`,分片标志)指示有效载荷是否在多个记录之间被 chunked(分片)。
Web NFC 会将所有接收到的分片记录转换为逻辑记录,并在需要时透明地对发送的有效载荷进行分片。
ME field(位 `6`,消息结束)指示该记录是否为 NDEF message 中的最后一个记录。
MB field(位 `7`,消息开始)指示该记录是否为 NDEF message 的第一个记录。
TYPE LENGTH field 是一个无符号 8 位整数,表示 TYPE field 的字节大小。
TYPE field 是一个全局唯一且维护的标识符,用以描述 PAYLOAD field 的类型,其结构、编码和格式由 TNF field 的值决定。
[[[!NFC-RTD]]] 要求 TYPE field 的名称比较必须以不区分大小写的方式进行。
ID LENGTH field 是一个无符号 8 位整数,表示 ID field 的字节大小。
ID field 是以 URI 引用 ([[RFC3986]]) 形式的标识符,具有唯一性,可以是绝对或相对的(在后者情况下,应用必须提供基 URI)。中间和终止的分片记录必须不具有 ID field,其他记录可以具有该字段。
PAYLOAD LENGTH field 表示 PAYLOAD field 的字节大小。如果 SR field 为 `1`,则其大小为 1 字节,否则为 4 字节,分别表示 8 位或 32 位无符号整数。
PAYLOAD field 承载应用字节。NDEF 对数据的任何内部结构保持不透明。注意在后文讨论的某些情况下,该字段可以包含作为数据的 NDEF message。
空记录 的 TYPE LENGTH field、ID LENGTH field 和 PAYLOAD LENGTH field 必须为 `0`,因此 TYPE field、ID field 和 PAYLOAD field 不得存在。
NFC Forum 在 [[NFC-RTD]](资源类型定义规范)中标准化了一些有用的子记录类型,称为 规范类型记录,例如文本、URL、媒体等。此外,还有为更复杂交互设计的记录类型,如智能海报(包含用于 url、text、signature 和 actions 的可选嵌入记录)和交接(handover)记录。
存储在 TYPE field 的 规范类型记录 的类型信息可以有两种: 本地类型 和 全局类型。
NFC Forum 的 本地类型 由 NFC Forum 或应用定义,并且总是以小写字母或数字开头。这些通常是简短字符串,仅在包含记录的本地上下文中唯一。当类型含义在包含记录的本地上下文之外不重要且存储使用是硬性约束时使用本地类型。有关如何使用本地类型的示例,请参见 Smart poster。
因此,[=local type=] 是根据包含记录类型定义的,因此不需要任何命名空间。基于此,相同的本地类型名称可以在另一个记录类型中以不同含义和不同有效载荷类型重复使用。
NFC Forum 的 全局类型 由 NFC Forum 定义和管理,通常以大写字母开头。 示例:"`T`" 表示文本,"`U`" 表示 URL,"`Sp`" 表示智能海报,"`Sig`" 表示签名,"`Hc`" 表示交接载体,"`Hr`" 表示交接请求,"`Hs`" 表示交接选择,等等。
URI 记录 在 [[NDEF-URI]] 中定义。 TNF field 为 `1`,TYPE field 为 "`U`"(`0x55`)。 PAYLOAD field 的第一个字节是 URI 标识符代码,实际上是缩写表中的索引,其值会预先加入到 URI 的其余部分。例如,值 `0` 表示不添加前缀,`1` 表示 "`http://www.`",`0x04` 表示 "`https://`" 等等。 有效载荷的其余部分包含作为 UTF-8 字符串的 URI 的其余部分(如果第一个字节为 `0`,则表示整个 URI)。
该 URI 在 [[RFC3987]] 中定义,实际上是一个 UTF-8 编码的 IRI,可以是 URN 或 URL。
[[NDEF-SMARTPOSTER]] 规定,当 NDEF message 中存在 smart poster 记录且该消息中还包含其他 URI records 时,应用程序应该仅使用该 smart poster 记录。
| 值 | 描述 |
|---|---|
| 0 | 执行该动作 |
| 1 | 稍后保存 |
| 2 | 打开以供编辑 |
| 3..0xFF | 预留用于将来使用 |
在 NDEF 标准化时,值 `0`(“执行该动作”)原本用于诸如发送 SMS、拨打电话或启动浏览器等用例。类似地,值 `1`(“稍后保存”)用于诸如将 SMS 存入收件箱、将 URL 保存到书签或将电话号码保存到联系人等用例。此外,值 `2`(“打开以供编辑”)旨在使用默认应用打开智能海报内容以进行编辑。
实现不需要对此处定义的动作实现任何标准化行为。在此 API 中,由应用决定其定义的动作(可能包括上述用例)。Web NFC 仅提供这些值。
NDEF Signature 在 [[NDEF-SIGNATURE]] 中定义。 其 TYPE field 包含 "`Sig`"(`0x53`, `0x69`, `0x67`),其 PAYLOAD field 包含版本、签名和证书链。
如当前规范所述,该项不被支持。
NFC handover 在 [[NFC-HANDOVER]] 中定义,对应的消息结构允许协商并激活备用通信载体,例如蓝牙或 WiFi。随后协商出的通信载体将被(单独地)用于在两个设备之间执行某些活动,例如向另一设备发送照片、向蓝牙打印机打印或向电视流式传输视频。
如当前规范所述,该项不被支持。
在 绝对 URL 记录 中,TYPE field 包含 绝对 URL 字符串,而不是有效载荷。
注意:某些平台(如 Windows Phone)在有效载荷中存储了额外的数据,但这些记录中的任何有效载荷数据在其他平台(如 Android)上会被忽略。在 Android 上读取此类记录时,会尝试在 Chrome 中加载该 URL,因此并不打算供客户端应用使用。
NFC Forum 的 外部类型记录 用于应用指定的数据类型,并在 [[[NFC-RTD]]] 中定义。
外部类型 是以前缀 `"urn:nfc:ext:"` 开头的 URN,后跟所有者的名称 [=domain=],再添加一个 `U+003A`(`:`),然后是非零的类型名,例如 `"urn:nfc:ext:w3.org:atype"`,在 TYPE field 中存储为 `"w3.org:atype"`。
未知记录 是存储不带关联 MIME type 的不透明数据的记录,这意味着可以假定默认的 `application/octet-stream` MIME type。[[NFC-NDEF]] 规范建议 NDEF 解析器将有效载荷存储或转发而不进行处理。
Web NFC 的任何实现必须将分片记录透明地呈现为单个逻辑记录。
已经在 此处 和 Web NFC 用例 文档中枚举了一些 NFC 用户场景。基本的 Web NFC 交互如下。
在使用 Web NFC 的 top-level browsing context 的 {{Document}} 可见时, 读取包含 NDEF message 的 NFC tag。例如,网页指示用户触碰 NFC 标签,然后从标签接收信息。
注意,对 NFC tag 的 NFC 写入操作总是同时涉及一次读取操作。
注意,将 NFC tag 永久设为只读总是涉及一次读取操作。
除了内置适配器外,用户可能为其设备附加一个或多个外部 NFC adapter。用户可以使用任意一个 NFC adapter。
本节展示开发者如何使用本规范的各项功能。
检测是否支持 Web NFC 可以通过检查 {{NDEFReader}} 对象来完成。注意,这并不保证 NFC 硬件可用。
if ("NDEFReader" in window) { /* Scan and write NDEF Tags */ }
写入数据通常很直接,但在 NFC 写入工作方式上有一些需要注意的地方。
NFC 读取器通过轮询工作,因此为了能够写入标签或将其永久设为只读,需要先找到并读取标签,这意味着需要首先初始化轮询。
如果尚未通过先调用 `scan()` 启动轮询,则 `write()` 和 `makeReadOnly()` 方法会临时启动轮询,直到找到并读取到标签并尝试执行操作为止。
这意味着流程是先在首次找到标签时执行一次读取,然后再执行写入操作。
这也意味着如果 `scan()` 正在运行且你为 `reading` 事件添加了事件监听器,那么在 `write()` 或 `makeReadOnly()` 操作期间该事件监听器会被触发一次,这可能不是预期的行为。
在接下来的各节中,我们将讨论如何轻松处理此行为,但先给出几个简单示例。
向 NFC 标签写入文本字符串很简单。
const ndef = new NDEFReader();
ndef.write(
"Hello World"
).then(() => {
console.log("Message written.");
}).catch(error => {
console.log(`Write failed :-( try again: ${error}.`);
});
为了写入 URL 类型的 NDEF 记录,只需使用 NDEFMessage。下面示例使用 async/await。
const ndef = new NDEFReader();
try {
await ndef.write({
records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }]
});
} catch {
console.log("Write failed :-( try again.");
};
为了写入,需要找到并读取标签。这使你能够通过检查现有数据或序列号来判断是否确实是要写入的标签。
因此,建议在 `reading` 事件中调用 `write()`。`makeReadOnly()` 也同理。
下例展示如何在通用的 `reading` 处理器和用于单次写入的处理器之间进行协调。
const ndef = new NDEFReader();
let ignoreRead = false;
ndef.onreading = (event) => {
if (ignoreRead) {
return; // write pending, ignore read.
}
console.log("We read a tag, but not during pending write!");
};
function write(data) {
ignoreRead = true;
return new Promise((resolve, reject) => {
ndef.addEventListener("reading", event => {
// Check if we want to write to this tag, or reject.
ndef.write(data).then(resolve, reject).finally(() => ignoreRead = false);
}, { once: true });
});
}
await ndef.scan();
try {
await write("Hello World");
console.log("We wrote to a tag!")
} catch(err) {
console.error("Something went wrong", err);
}
有时为写入操作设置时间限制会很有用。比如要求用户触碰标签,如果在一定时间内未找到标签,则超时。
const ndef = new NDEFReader();
ndef.onreading = (event) => console.log("We read a tag!");
function write(data, { timeout } = {}) {
return new Promise((resolve, reject) => {
const ctlr = new AbortController();
ctlr.signal.onabort = () => reject("Time is up, bailing out!");
setTimeout(() => ctlr.abort(), timeout);
ndef.addEventListener("reading", event => {
ndef.write(data, { signal: ctlr.signal }).then(resolve, reject);
}, { once: true });
});
}
await ndef.scan();
try {
// Let's wait for 5 seconds only.
await write("Hello World", { timeout: 5_000 });
} catch(err) {
console.error("Something went wrong", err);
} finally {
console.log("We wrote to a tag!");
}
本示例展示当 {{NDEFReader/scan}} promise 被拒绝并触发 `readingerror` 时会发生什么。
const ndef = new NDEFReader();
ndef.scan().then(() => {
console.log("Scan started successfully.");
ndef.onreadingerror = (event) => {
console.log("Error! Cannot read data from the NFC tag. Try a different one?");
};
ndef.onreading = (event) => {
console.log("NDEF message read.");
};
}).catch(error => {
console.log(`Error! Scan failed to start: ${error}.`);
});
本示例展示如何轻松创建一个便捷函数,该函数只读取单个标签然后停止轮询,从而通过减少不必要的工作来节省电量。
该示例可以很容易扩展为在给定毫秒后超时。
const ndef = new NDEFReader();
function read() {
return new Promise((resolve, reject) => {
const ctlr = new AbortController();
ctlr.signal.onabort = reject;
ndef.addEventListener("reading", event => {
ctlr.abort();
resolve(event);
}, { once: true });
ndef.scan({ signal: ctlr.signal }).catch(err => reject(err));
});
}
read().then(({ serialNumber }) => {
console.log(serialNumber);
});
本示例展示如何读取可存储在标签上的各种不同类型的数据。如果标签未格式化或包含空记录,则写入一条值为 "Hello World" 的文本消息。
const ndef = new NDEFReader();
await ndef.scan();
ndef.onreading = async ({ message }) => {
if (message.records.length == 0 || // unformatted tag
message.records[0].recordType == "empty") { // empty record
await ndef.write({
records: [{ recordType: "text", data: "Hello World" }]
});
return;
}
const decoder = new TextDecoder();
for (const record of message.records) {
switch (record.recordType) {
case "text":
const textDecoder = new TextDecoder(record.encoding);
console.log(`Text: ${textDecoder.decode(record.data)} (${record.lang})`);
break;
case "url":
console.log(`URL: ${decoder.decode(record.data)}`);
break;
case "mime":
if (record.mediaType === "application/json") {
console.log(`JSON: ${JSON.parse(decoder.decode(record.data))}`);
}
else if (record.mediaType.startsWith("image/")) {
const blob = new Blob([record.data], { type: record.mediaType });
const img = document.createElement("img");
img.src = URL.createObjectURL(blob);
img.onload = () => window.URL.revokeObjectURL(this.src);
document.body.appendChild(img);
}
else {
console.log(`Media not handled`);
}
break;
default:
console.log(`Record not handled`);
}
}
};
通过使用自定义记录标识符(在本例中为 "`/my-game-progress`")可以筛选相关数据源。当我们读取数据时,会立即通过使用自定义 NDEF 数据布局写入来更新游戏进度。
const ndef = new NDEFReader();
await ndef.scan();
ndef.onreading = async ({ message }) => {
if (message.records[0]?.id !== "/my-game-progress")
return;
console.log(`Game state: ${ JSON.stringify(message.records) }`);
const encoder = new TextEncoder();
const newMessage = {
records: [{
id: "/my-game-progress",
recordType: "mime",
mediaType: "application/json",
data: encoder.encode(JSON.stringify({
level: 3,
points: 4500,
lives: 3
}))
}]
};
await ndef.write(newMessage);
console.log("Message written");
};
使用序列化与反序列化存储和接收 JSON 数据很容易。
const ndef = new NDEFReader();
await ndef.scan();
ndef.onreading = (event) => {
const decoder = new TextDecoder();
for (const record of event.message.records) {
if (record.mediaType === "application/json") {
const json = JSON.parse(decoder.decode(record.data));
const article =/^[aeio]/i.test(json.title) ? "an" : "a";
console.log(`${json.name} is ${article} ${json.title}`);
}
}
};
const encoder = new TextEncoder();
await ndef.write({
records: [
{
recordType: "mime",
mediaType: "application/json",
data: encoder.encode(JSON.stringify({
name: "Benny Jensen",
title: "Banker"
}))
},
{
recordType: "mime",
mediaType: "application/json",
data: encoder.encode(JSON.stringify({
name: "Zoey Braun",
title: "Engineer"
}))
}]
});
写入数据需要触碰 NFC tag。
const ndef = new NDEFReader();
await ndef.scan();
ndef.onreading = async (event) => {
const decoder = new TextDecoder();
for (const record of event.message.records) {
console.log("Record type: " + record.recordType);
console.log("MIME type: " + record.mediaType);
console.log("=== data ===\n" + decoder.decode(record.data));
}
try {
await ndef.write("Overriding data is fun!");
} catch(error) {
console.log(`Write failed :-( try again: ${error}.`);
}
};
使用 {{NDEFScanOptions/signal}} 读取 NDEF 消息 3 秒。
const ndef = new NDEFReader();
const ctrl = new AbortController();
await ndef.scan({ signal: ctrl.signal });
ndef.onreading = () => {
console.log("NDEF message read.");
};
ctrl.signal.onabort = () => {
console.log("We're done waiting for NDEF messages.");
};
// Stop listening to NDEF messages after 3s.
setTimeout(() => ctrl.abort(), 3_000);
const ndef = new NDEFReader();
const encoder = new TextEncoder();
await ndef.write({ records: [
{
recordType: "smart-poster", // Sp
data: { records: [
{
recordType: "url", // URL record for the Sp content
data: "https://my.org/content/19911"
},
{
recordType: "text", // title record for the Sp content
data: "Funny dance"
},
{
recordType: ":t", // type record, a local type to Sp
data: encoder.encode("image/gif") // MIME type of the Sp content
},
{
recordType: ":s", // size record, a local type to Sp
data: new Uint32Array([4096]) // byte size of Sp content
},
{
recordType: ":act", // action record, a local type to Sp
// do the action, in this case open in the browser
data: new Uint8Array([0])
},
{
recordType: "mime", // icon record, a MIME type record
mediaType: "image/png",
data: await (await fetch("icon1.png")).arrayBuffer()
},
{
recordType: "mime", // another icon record
mediaType: "image/jpg",
data: await (await fetch("icon2.jpg")).arrayBuffer()
}
]}
}
]});
外部类型记录可以用于创建应用定义的记录。这些记录可以包含一个作为有效载荷的 NDEF message, 其内部可以有自己的 NDEF records,包括在应用上下文中使用的 local types。
注意,smart poster 记录类型也包含一个作为有效载荷的 NDEF message。
由于 NDEF 不保证记录的顺序,使用包含 NDEF message 作为有效载荷的外部类型记录,对于封装相关数据可能很有用。
本示例展示如何读取用于社交发布的外部记录,该记录包含一个 NDEF message,其中包含一个文本记录和一个具有 local type "act"(动作)的记录, 其定义借用自 smart poster,但在本地应用上下文中使用。
const ndef = new NDEFReader();
await ndef.scan();
ndef.onreading = (event) => {
const externalRecord = event.message.records.find(
record => record.type == "example.com:smart-poster"
);
let action, text;
for (const record of externalRecord.toRecords()) {
if (record.recordType == "text") {
const decoder = new TextDecoder(record.encoding);
text = decoder.decode(record.data);
} else if (record.recordType == ":act") {
action = record.data.getUint8(0);
}
}
switch (action) {
case 0: // do the action
console.log(`Post "${text}" to timeline`);
break;
case 1: // save for later
console.log(`Save "${text}" as a draft`);
break;
case 2: // open for editing
console.log(`Show editable post with "${text}"`);
break;
}
};
外部类型记录可以用于创建应用定义的记录,这些记录甚至可以包含一个作为有效载荷的 NDEF message。
const ndef = new NDEFReader();
await ndef.write({ records: [
{
recordType: "example.game:a",
data: {
records: [
{
recordType: "url",
data: "https://example.game/42"
},
{
recordType: "text",
data: "Game context given here"
},
{
recordType: "mime",
mediaType: "image/png",
data: getImageBytes(fromURL)
}
]
}
}
]});
未知类型记录在外部类型记录内可能很有用,因为开发者知道它们代表什么,因此可以避免指定 MIME 类型。
const encoder = new TextEncoder();
const ndef = new NDEFReader();
await ndef.write({ records: [
{
recordType: "example.com:shoppingItem", // External record
data: {
records: [
{
recordType: "unknown", // Shopping item name
data: encoder.encode("Food")
},
{
recordType: "unknown", // Shopping item description
data: encoder.encode("Provide nutritional support for an organism.")
}
]
}
}
]});
const ndef = new NDEFReader();
await ndef.scan();
ndef.onreading = (event) => {
const shoppingItemRecord = event.message.records[0];
if (shoppingItemRecord?.recordType !== "example.com:shoppingItem")
return;
const [nameRecord, descriptionRecord] = shoppingItemRecord.toRecords();
const decoder = new TextDecoder();
console.log("Item name: " + decoder.decode(nameRecord.data));
console.log("Item description: " + decoder.decode(descriptionRecord.data));
};
将 NFC 标签永久设为只读很直接。
const ndef = new NDEFReader();
ndef.makeReadOnly().then(() => {
console.log("NFC tag has been made permanently read-only.");
}).catch(error => {
console.log(`Operation failed: ${error}`);
});
const ndef = new NDEFReader();
try {
await ndef.write("Hello world");
console.log("Message written.");
await ndef.makeReadOnly();
console.log("NFC tag has been made permanently read-only after writing to it.");
} catch (error) {
console.log(`Operation failed: ${error}`);
}
任意 NDEF message 的内容由 NDEFMessage 接口暴露:
[SecureContext, Exposed=Window]
interface NDEFMessage {
constructor(NDEFMessageInit messageInit);
readonly attribute FrozenArray<NDEFRecord> records;
};
dictionary NDEFMessageInit {
required sequence<NDEFRecordInit> records;
};
records 属性表示定义该 NDEF message 的一列 NDEF record。
NDEFMessageInit 字典用于初始化一个 NDEF message。
任意 NDEF record 的内容由 NDEFRecord 接口暴露:
[SecureContext, Exposed=Window]
interface NDEFRecord {
constructor(NDEFRecordInit recordInit);
readonly attribute USVString recordType;
readonly attribute USVString? mediaType;
readonly attribute USVString? id;
readonly attribute DataView? data;
readonly attribute USVString? encoding;
readonly attribute USVString? lang;
sequence<NDEFRecord>? toRecords();
};
dictionary NDEFRecordInit {
required USVString recordType;
USVString mediaType;
USVString id;
USVString encoding;
USVString lang;
any data; // DOMString or BufferSource or NDEFMessageInit
};
mediaType 属性表示 NDEF record 有效载荷的 MIME type。
recordType 属性表示 NDEF record 的类型。
NFC NDEF 规范使用术语“message identifier”和“payload identifier”而不是 record identifier,但该标识符与每个记录相关联而非与消息(记录集合)相关联,并且在没有有效载荷时也可能存在。
encoding 属性表示在有效载荷为文本数据时用于对其进行编码的 [=encoding/name|encoding name=]。
lang 属性表示在有效载荷已编码时的 [=language tag=]。
language tag 是一个匹配 [[BCP47]] 规范中 Language-Tag 产生式的 string(关于可能值的权威列表,请参见
IANA Language Subtag
Registry)。语言范围由一个或多个由 U+002D HYPHEN-MINUS ("-") 分隔的 subtags
组成。例如,'en-AU' 表示澳大利亚的英语,'fr-CA' 表示加拿大的法语。在不参考 IANA Language Subtag Registry
的情况下,可根据 [[RFC5646]] 第 2.2.9 节验证其结构有效性的语言标签被视为结构上有效。
data 属性表示 PAYLOAD field 的数据。
toRecords() 方法在被调用时,必须返回对给定 NDEF Record 运行 convert NDEFRecord.data bytes 的结果。
NDEFRecordInit 字典用于使用其 record type recordType、可选的 record identifier id 和有效载荷数据 data 来初始化一个 NDEF record。
从 NDEFRecordInit 的数据类型映射到 NDEF record 的规则在处理数据的算法步骤中给出,并在 [[[#steps-receiving]]] 和 [[[#writing-content]]] 节中描述。
要对给定 |record:NDEFRecord| 执行 convert NDEFRecord.data bytes,请运行以下步骤:
该字符串定义了 NDEFRecord 的允许记录类型。[[[#data-mapping]]] 节描述了它如何映射到 NDEF record 类型。
一个标准化的 well known type name 可以是以下之一:
除了 [=well known type names=],组织也可以创建自定义的 external type name,它是由 [=domain=] 名称和自定义类型名组成的字符串,二者以冒号 `U+003A` (`:`) 分隔。
应用也可以使用 local type name,该字符串必须以小写字符或数字开头,表示 NFC Forum [=local type=] 的类型。它通常用于作为父 NDEFRecord 有效载荷的 NDEFMessage 中的记录,例如在 smart poster 中。local type 的上下文是其父记录(其有效载荷为包含该记录的 NDEFMessage),并且 local type name 不应与该上下文中使用的任何其他类型名冲突。
Web NFC 的任何实现必须将分片记录透明地呈现为单个逻辑记录,因此 unchanged record 不会被显式表示。
两个 well-known type records(包括任何 NFC Forum 的 local type 和任何 NFC Forum 的 global type)必须逐字符以区分大小写的方式比较。
两个 external types 必须逐字符以不区分大小写的方式比较。
任何 well-known type record 和 external type 的二进制表示必须作为相对 URI(RFC 3986)写入,分别省略命名空间标识符 (NID) "`nfc`" 与命名空间特定字符串 (NSS) "`wkt`" 和 "`ext`",即省略 "`urn:nfc:wkt:`" 与 "`urn:nfc:ext:`" 前缀。例如,"`urn:nfc:ext:company.com:a" 存储为 "`company.com:a`",而 Text record 的 well-known type records 为 "`urn:nfc:wkt:T`",但存储为 "`T`"。
从 NDEFRecordInit 的数据类型到 NDEF record 类型的映射(在 [[[#writing-content]]] 节中使用)如下:
| {{recordType}} | {{mediaType}} | {{data}} | record type | [=TNF field=] | [=TYPE field=] |
|---|---|---|---|---|---|
| "`empty`" | 未使用 | 未使用 | Empty record | 0 | 未使用 |
| "`text`" | 未使用 |
{{BufferSource}} 或 {{DOMString}} |
[=Well-known type record=] | 1 | "`T`" |
| "`url`" | 未使用 | {{DOMString}} | [=Well-known type record=] | 1 | "`U`" |
| "`smart-poster`" | 未使用 | {{NDEFMessageInit}} | [=Well-known type record=] | 1 | "`Sp`" |
| [=local type name=] 前缀为冒号 `U+003A` (`:`),例如 "`:act`"、"`:s`" 和 "`:t`" | 未使用 | {{BufferSource}} 或 {{NDEFMessageInit}} | [=Local type=] record* | 1 | [=local type name=],例如 "`act`"、"`s`" 和 "`t`" |
| "`mime`" | [= MIME type =] | {{BufferSource}} | MIME type record | 2 | [= MIME type =] |
| "`absolute-url`" | 未使用 | {{DOMString}} url | Absolute-URL record | 3 | [=Absolute-URL=] |
| [=external type name=] | 未使用 |
{{BufferSource}} 或 {{NDEFMessageInit}} |
External type record | 4 | [=external type name=] |
| "`unknown`" | 未使用 | {{BufferSource}} | [=Unknown record=] | 5 | 未使用 |
* 一个 [=local type=] 记录必须嵌入在另一个记录的 NDEFMessage 有效载荷中。
从 NDEF record 类型到 NDEFRecord 的映射(用于描述传入的 NDEF message 的 [[[#steps-receiving]]] 节)如下。
| record type | [=TNF field=] | [=TYPE field=] | {{recordType}} | {{mediaType}} |
|---|---|---|---|---|
| [=Empty record=] | 0 | 未使用 | "`empty`" | null |
| [=Well-known type record=] | 1 | "`T`" | "`text`" | null |
| [=Well-known type record=] | 1 | "`U`" | "`url`" | null |
| [=Well-known type record=] | 1 | "`Sp`" | "`smart-poster`" | null |
| [=Local type=] record* | 1 | [=local type name=],例如 "`act`"、"`s`" 和 "`t`" | [=local type name=] 前缀为冒号 `U+003A` (`:`),例如 "`:act`"、"`:s`" 和 "`:t`" | null |
| [=MIME type record=] | 2 | [=MIME type=] | "`mime`" | NDEF 记录中使用的 MIME type |
| [=Absolute-URL record=] | 3 | URL | "`absolute-url`" | null |
| [=External type record=] | 4 | [=external type name=] | [=external type name=] | null |
| Unknown record | 5 | 未使用 | "`unknown`" | null |
NDEFReader 是一个将 NFC 功能暴露给browsing context 的对象:当设备(如标签)位于磁感应场内时读取 NDEF messages。此外,它也用于向范围内的 NFC tag 写入 NDEF messages。
typedef (DOMString or BufferSource or NDEFMessageInit) NDEFMessageSource;
[SecureContext, Exposed=Window]
interface NDEFReader : EventTarget {
constructor();
attribute EventHandler onreading;
attribute EventHandler onreadingerror;
Promise<undefined> scan(optional NDEFScanOptions options={});
Promise<undefined> write(NDEFMessageSource message,
optional NDEFWriteOptions options={});
Promise<undefined> makeReadOnly(optional NDEFMakeReadOnlyOptions options={});
};
[SecureContext, Exposed=Window]
interface NDEFReadingEvent : Event {
constructor(DOMString type, NDEFReadingEventInit readingEventInitDict);
readonly attribute DOMString serialNumber;
[SameObject] readonly attribute NDEFMessage message;
};
dictionary NDEFReadingEventInit : EventInit {
DOMString? serialNumber = "";
required NDEFMessageInit message;
};
NDEFMessageSource 是一个联合类型,表示 {{NDEFReader/write()}} 方法所接受的参数类型。
NDEFReadingEvent 是在有新的 NFC 读取时分派的事件。serialNumber 属性表示用于防冲突和标识的设备序列号;若不可用则为空字符串。message 是一个 NDEFMessage 对象。
NDEFReadingEventInit 用于通过成员 message 以序列号和 NDEFMessageInit 数据来初始化新事件。如果未提供或为 null 的 serialNumber,则使用空字符串初始化事件。
虽然大多数标签都有稳定的唯一标识符(UID),但并非所有标签都有,有些标签甚至每次读取都会生成一个随机数。序列号通常由 4 或 7 个数字组成,以 `:` 分隔。
一个 {{NDEFReader}} 对象具有以下 内部槽位:
| Internal Slot | Initial value | Description (non-normative) |
|---|---|---|
| [[\WriteOptions]] | null | 写入所用的 {{NDEFWriteOptions}} 值。 |
| [[\WriteMessage]] | null | 将要写入的 {{NDEFMessage}}。初始未设置。 |
onreading 是一个 {{EventHandler}},用于在有新读取可用时通知。
onreadingerror 是一个 {{EventHandler}},用于在读取期间发生错误时通知。
支持 NFC 的active document 的relevant settings object 关联一个 NFC state 记录,其包含以下内部槽位:
| Internal Slot | Initial value | Description (non-normative) |
|---|---|---|
| [[\Suspended]] | false | 一个布尔标志,指示 NFC 功能是否suspended,初始为 false。 |
| [[\ActivatedReaderList]] | 空set | 一个 {{NDEFReader}} 实例的set。 |
| [[\PendingWrite]] | empty | 一个 <|promise:Promise|, |writer:NDEFReader|> 元组,其中 |promise| 保存一个挂起的 {{Promise}},|writer| 保存一个 {{NDEFReader}}。 |
| [[\PendingMakeReadOnly]] | empty | 一个 <|promise:Promise|, |writer:NDEFReader|> 元组,其中 |promise| 保存一个挂起的 {{Promise}},|writer| 保存一个 {{NDEFReader}}。 |
activated reader objects 是 [[\ActivatedReaderList]] 内部槽位的值。
pending write tuple 是 [[\PendingWrite]] 内部槽位的值。
pending makeReadOnly tuple 是 [[\PendingMakeReadOnly]] 内部槽位的值。
若 [[\Suspended]] 内部槽位为 true,则表示NFC is suspended。
要 suspend NFC,将 [[\Suspended]] 内部槽位设为 true。
要 resume NFC,将 [[\Suspended]] 内部槽位设为 false。
内部槽位仅作为本规范中的记法使用,实现不必将其映射为显式的内部属性。
Web NFC API 是一个 [=default powerful feature=],其 [=powerful feature/name=] 为 "nfc"。
nfc" [=getting the current permission state=]
的结果。
nfc" 向用户请求权限。如获授予,则返回 true。
request permission 步骤尚未明确定义。此时 UA 会就给定
origin 和 global object 使用 "nfc" 的策略询问用户;若用户授予权限,则返回 true。
本规范的 [=page visibility change steps=],给定字符串 |visibilityState| 和 {{Document}} |document|,如下:
术语 suspended 指 NFC 操作被暂停,这意味着在被暂停期间,任何 NDEFReader 都不会写入 NFC content,且不会向任何 {{NDEFReader}} 呈现接收到的 NFC content。
拒绝该 promise 将清除 pending write tuple。
拒绝该 promise 将清除 pending makeReadOnly tuple。
要在environment settings object 上release NFC,执行以下步骤:
UA 必须在文档的relevant settings object 上作为额外的unloading document cleanup steps 来release NFC。
dictionary NDEFWriteOptions {
boolean overwrite = true;
AbortSignal? signal;
};
当 overwrite 属性的值为 false 时,write 算法 将读取 NFC tag 以确定其上是否已有 NDEF 记录;若有,则不会执行任何挂起的写入。
signal 属性允许中止 {{NDEFReader/write()}} 操作。
dictionary NDEFMakeReadOnlyOptions {
AbortSignal? signal;
};
signal 属性允许中止 {{NDEFReader/makeReadOnly()}} 操作。
dictionary NDEFScanOptions {
AbortSignal signal;
};
signal 属性允许中止 {{NDEFReader/scan()}} 操作。
本节描述如何在定时器到期前,当某个 NFC tag 下次进入近距离范围时,向其写入一个 NDEF message。任一时刻,对于一个 origin 最多只能设置一个待写入的 NDEF message,直至当前消息被发送或写入被中止。
UA 可能在此时中止写入。终止的原因属于实现细节。例如,实现可能无法支持所请求的操作。
一次写入会替换所有先前配置的写入操作。
若 NFC is suspended,继续等待,直到用户中止 promise 或某个 NFC tag 进入通信范围。
若近距离范围内的 NFC tag 未格式化且可进行 NDEF 格式化,则先格式化并将 |output| 作为缓冲区写入。
多个适配器应由用户依次使用。几乎不太可能在两个或多个不同且已连接的 NFC adapter 上同时轻触。若确实发生,用户可能需要重复轻触,最好一次一个设备。此处的错误表明需要重试,否则用户可能认为操作在所有连接的 NFC adapter 上都成功了。
Web NFC 当前允许在 smart poster 中写入 external type 和 local type 记录。同时,也允许 empty records。应用可以忽略 smart poster 内的任何额外记录。
图标记录的媒体类型可以被限制为 `"image/"` 或 `"video/"`,但 [[NDEF-SMARTPOSTER]] 规范实际上允许在 smart poster 中使用其他媒体类型记录,这些可以由应用特定方式处理,例如使用其关联的某种 MIME types 的 vCard 名片。
[[NFC-RTD]] 也规定了 URN 前缀 “`urn:nfc:ext:`”,但它不会存储在 NDEF record 中,因此 Web NFC 应用在创建 external type records 时不应(SHOULD NOT)指定该 URN 前缀。
[[NFC-RTD]] 要求 external type 名在表示时带有 URN 前缀 “`urn:nfc:ext:`”,例如在读取 NDEF messages 时。然而,由于 external type records 通过 TNF FIELD 被设为 `0x04` 来区分,类型名冲突的风险很低。此外,W3C TAG 也建议在 Web 上避免使用 URN。因此,Web NFC 在读取或写入 NDEF messages 时都不使用该 URN 前缀。
给定 |input:USVString|,要split external type,执行以下步骤:
给定 |input:USVString|,要validate external type,执行以下步骤:
给定 |input:USVString|,要validate local type,执行以下步骤:
当客户端特别希望以 [=well-known type record=] 写入文本时这很有用。其他选项包括使用 "`mime`" 值并显式指定文本类 MIME type,这有助于更好地区分,例如使用 "`text/xml`" 或 "`text/vcard`"。
[[NDEF-SMARTPOSTER]] 规范只允许在一个 smart poster 中包含一个 URL,且该 URL 必须是单个 URI record。
本节描述当某个 NFC tag 在近距离范围内时,如何将其永久设为只读。任一时刻,对于一个 origin 最多只能有一个请求,直到该 NFC tag 被永久设为只读或操作被中止。
UA 可能在此时中止。终止的原因属于实现细节。例如,实现可能无法支持请求的操作。
一次只读化操作会替换所有先前配置的只读化操作。
若 NFC is suspended,继续等待,直到用户中止 promise 或某个 NFC tag 进入通信范围。
此操作不可逆。一旦某个 NFC 标签被设为只读,就无法再写入。
要监听 NFC content,客户端必须通过调用 NDEFReader.scan() 激活一个 {{NDEFReader}} 实例。当在其上附加 "`reading`" 事件的监听器时,客户端即可访问 NFC content。
若在 activated reader objects 中存在任意 {{NDEFReader}} 实例,则 UA 必须在所有已连接的 NFC 适配器上监听 NDEF message。
传入的 NFC content 会通过 {{NDEFReader}} 实例进行匹配。
UA 应(SHOULD)将未格式化的 NFC tag 表示为一个不包含 NDEF record 的 NDEF message,即其 {{NDEFMessage/records}} 属性为一个空数组。
给定类型为 serialNumber 的 |serialNumber:serialNumber| 和类型为 NDEFMessage 的 |message:NDEFMessage|,要dispatch NFC content,执行以下步骤:
由于分片记录不允许作为子记录,忽略位 5(CF field)。
应用可以对 data 调用 toRecords() 将其解析为 NDEF records,或自行解析。
应用可以将该值解析为一个 32 位无符号整数,表示 smart-poster 中 URI 记录所引用对象的大小。
应用可以将该值解析为一个字符串,包含 [[RFC2048]] 媒体类型,表示 smart-poster 中 URI 记录所引用对象的媒体类型。
应用可以将该值解析为一个 8 位无符号整数,其取值定义见此处。
本规范依赖一个阻止列表文件来限制网站可访问的 NFC 设备集合。
在 |url:URL| 处解析阻止列表的结果是一个历史字节十六进制值的列表,由以下算法生成:
blocklist 是在 https://github.com/w3c/web-nfc/blob/gh-pages/blocklist.txt 处parsing the blocklist 的结果。UA 应定期重新获取阻止列表,但获取频率未作规定。
若 blocklist 的值包含某设备的 historical bytes 十六进制值,则该 NFC device 被视为 blocklisted。在 ISO 14443-4 术语中,historical bytes 是 RATS(Request for Answer To Select)响应的一个子集。
实现需要确保当用户授权 Web NFC API 的某个方法时,只执行该操作且无任何副作用。
默认情况下,NDEF 并不提供使内容被信任的机制,除了允许在写入数据后将标签永久设为只读。这甚至可以在工厂设置中完成。
通过此 API 写入的数据不会自动签名或加密,这与现有原生 NFC API 一致。为保护 NDEF 消息的完整性与真实性,NFC 论坛引入了 [[NDEF-SIGNATURE]]。使用 NDEF signature 和密钥管理由应用自行负责。
若要信任通过 NFC 交换的数据的机密性,应用可以使用加密的 NFC content。
若要信任通过 NFC 交换的数据的完整性,应用可以使用 NDEF signature,并基于公钥基础设施(PKI)进行密钥管理。
关于 MIME 类型的一般安全注意事项见 [[RFC2048]] 和 [[RFC2046]]。
与条形码或二维码的比较是恰当的,因为 NFC 标签是另一种非人可读的数据交换方式,共享它们可能带来意想不到的隐私与安全影响。网站读取二维码时,必须使用打断式 UI(相机)来拍摄图像,使用户清楚该图像(包括二维码)的内容将可供网页使用,从而明确告知用户正在进行扫描。
使用 NFC 扫描标签需要用户将扫描设备(如手机)置于 NFC 标签的近距离范围内——通常为 5-10 厘米(2-4 英寸)。
当未激活 Web NFC 扫描时扫描标签,会触发主机操作系统处理。因此,从 NFC 标签扫描并启动 URL 或应用并非由 Web NFC 自身处理或支持。
此外,Web NFC 的扫描需要由用户交互触发,且当网站不在焦点或设备屏幕熄灭(即设备未解锁)时,扫描会被暂停。这样设计是为了降低偶发扫描的可能性。
Web NFC 还建议实现从用户体验角度(UX)清晰告知:当把扫描设备靠近 NFC 标签时会进行数据扫描——基本上模仿扫描二维码的 UX 流程。
可采用多种方式实现,例如播放提示音,或在扫描可能发生时显示持续的 UI,比如带有随时可取消能力的模态对话框。
实现还可以显示即将上传的数据,推迟数据分享直到用户确认,甚至显示允许用户选择要分享的记录的 UI。
当用户扫描标签时,网页应用可读取标签上的数据;若其不是只读,也可以向标签写入数据。面向私人使用的消费者贴纸(例如创客社区)通常是未上锁的(可读+可写),而商业部署的 NFC 通常是只读的。
旧协议 SNEP(Simple NDEF Exchange Protocol)允许有源设备(如手机)从另一有源设备接收 NDEF 数据,但 Web NFC 不支持它,且在受支持的原生平台上也在被弃用。
新协议 TNEP(Tag NDEF Exchange Protocol)允许扫描设备(如手机)与主动供电的设备(如 IoT 设备)进行双向通信。Web NFC 目前不支持它,且该协议对可接受的输入有限制,IoT 设备必须确保所接受的记录有效。
如果标签包含隐私敏感数据,则该数据会被分享给站点。如果 UX 要求用户在执行前确认数据交换,则可能不会立即分享。
某些情况下,标签/设备包含隐私敏感数据是显而易见的,例如配备 NFC 的会议胸牌和名片。又如配备 NFC 的血糖仪,可能表明你或近亲为糖尿病患者。
其他情况下,这一点可能不明显,但用户可能曾使用应用或网站向标签写入数据,而该数据在用户不知情的情况下编码了用户 ID 等信息,之后任何其他站点都可能读取到。
私密和意外的数据也可能存储在文件中(例如文字处理文档、PDF 或相机图像),并通过文件上传 API 上传。与文件上传相比,Web NFC API 所采用的缓解措施更强,且数据更不可能与个人直接关联。
对标签的扫描还可能暴露用户位置——若网站能够识别标签并知道该标签在现实世界中的位置(例如安装在博物馆内部)。也可能通过某些线索推断位置,例如 FeliCa NFC 标签多用于日本,但 Web NFC 并不会透露所用的标签技术。
这并不会把网络广告与跟踪模型带入现实世界,因为这需要用户的操作,且不能在后台触发;通过良好的 UX 应清楚地表明扫描处于激活状态。
还存在担忧:向 NFC 标签写入可能损坏或“变砖”。由于 NFC 标签被设计为可由多个用户应用读取,NDEF 标签提供了便捷方式可将设备永久设为只读,甚至可在工厂中配置为只读。
NDEF 是用于读写数据的简单交换格式,而非双向通信。NFC 支持多种基于更低层技术的通信格式(因此不像 NDEF 那样被锁定为只读),且这些格式均不受 Web NFC 支持。
本节列出用户在使用 NFC 时应当了解的一些事项。建议实现方在执行相关 NFC 操作之前或期间,帮助用户了解这些事实。
当站点有权读取 NFC 内容时,被扫描标签的数据将与该站点共享,类似上传文件和图片。与任何站点一样,是否信任站点以适当、预期的方式处理这些数据取决于用户。
已部署的 NFC 方案(例如商店中的标签)应始终设为只读,以确保不会因错误或恶意行为而被修改。
私人标签和贴纸往往出厂即未上锁(可写),用户应意识到此类标签在被扫描时可能被覆盖/修改。
固定标签可能在数据中编码其 ID 或位置,这意味着读取该标签会将此信息暴露给知道该标签物理位置的站点,从而推断读取发生的位置。这与登录某项服务相结合,可能将你的位置信息分享给该站点。
写入的数据可被其他具有读取权限的应用和站点读取。任何标签上的 NDEF 数据都可被具有相应访问权限的任何应用或网站读取,因此若不希望如此,数据应以安全方式加密,使仅有被授权者能够读取。
NFC 一次只能读取一个标签,但可以检测到多个标签,并可在其中选择一个进行通信。
这类用例包括:钱包中有多张智能卡(基于 NFC),但不想把卡取出。
这主要适用于由外部硬件读取的支付卡和交通卡,因此不是 Web NFC 的用例。对于 Web NFC,当存在多个可用标签时我们不允许读取,以防以下攻击向量。
存在一种攻击向量:有人将另一个恶意 NFC 标签/贴纸覆盖在合法标签上,以加载错误的应用/站点,或向正确的应用/站点注入错误数据。他们可以通过克隆原标签数据并加以修改来实现——要么将 URL
改为加载恶意应用/站点,要么更改数据以向正确的应用/站点注入恶意数据。示例:标签本应带你访问 https://example.com 却被修改为
https://exаmple.com(其中包含西里尔字母 а)——看起来很像真的,你可能会向恶意站点提供敏感数据。
从标签加载网站不在 Web NFC 的范围内,但建议用户代理在存在多个标签时不要自动加载 URL,以避免上述攻击向量。
通过在存在多个标签时禁止读取,Web NFC 能较好地防止向站点注入错误/恶意数据,因为屏蔽现有 NFC 标签相当困难,需要铁氧体屏蔽且较为明显。金属会干扰磁场,使标签不可读。
已考虑以下攻击者模式:
NFC 安全简介见此处。Web NFC 的潜在威胁如下。
实现应(SHOULD)使用一种机制来 obtain permission,例如由用户显式授予的权限。建议 UA 使用 [[[PERMISSIONS]]] API 来实现与 NFC 相关的权限。
实现可(MAY)使用按会话/临时权限。
实现可(MAY)在网页正在访问 NFC 适配器(例如存在进行中的扫描)时显示覆盖式对话框,以提醒用户。
为信任通过 NFC 交换的数据的机密性,应用可以使用基于公钥基础设施(PKI)进行密钥管理的加密 NFC content。密钥管理不在 Web NFC 的范围内。
为信任通过 NFC 交换的数据的完整性,用户代理可(MAY)使用基于公钥基础设施的 NDEF signature 进行密钥管理。
对于使用 NDEF signature 1.0 版本([[NFC-SECURITY]])签名的标签,其签名仅覆盖 TYPE field、ID field 与 PAYLOAD field,未覆盖 NDEF 头部的第一个字节,从而留下攻击面。[[NFC-SECURITY]] 的 2.0 版本将标签硬件属性纳入签名并允许使用更短的证书。
一个 NDEF signature 覆盖其之前的记录,直到遇到另一个 NDEF signature 或消息起始(NDEF message 的开头)。
为缓解 已知漏洞,建议应用始终使用单个 NDEF signature 为整个 NDEF message 签名,并使用正确的工具链与安全策略来创建与验证签名。
本节列出实现的规范性安全策略。
只有secure contexts才允许访问 NFC content。浏览器可(MAY)仅为开发目的而忽略此规则。
Web NFC 功能仅允许用于 top-level browsing context 的 {{Document}},且其 {{Document/visibilityState}} 为 `"visible"`。
这也意味着,当显示关闭或设备锁定时,UA 应阻止对 NFC 射频的访问。对于在后台的网页,接收与写入 NFC content 必须被suspended。
将 NFC tag 永久设为只读必须(MUST)obtain permission,否则失败。见 [[[#making-content-read-only]]] 一节。
设置用于读取 NFC content 的监听器应当(SHOULD)obtain permission。
将 NFC content 写入 NFC tag 必须(MUST)obtain permission。见 [[[#writing-content]]] 一节。
所有超出当前浏览会话而被保留的权限必须(MUST)可被撤销。
Web NFC 包含一个 blocklist,用于阻止存在漏洞的 NFC 设备被网站利用。
在监听与写入 NFC content 时,UA 可(MAY)警示用户:给定的 origin 可能能够推断物理位置。
当 NFC content 的载荷数据不受信任时,UA 不得(MUST NOT)使用其自动处理内容,例如自动打开 NFC tag 中发现的 URL 所指向的网页、安装应用或执行其他操作,除非用户批准。
编辑谨此感谢 Jeffrey Yasskin、Anne van Kesteren、Anssi Kostiainen、Domenic Denicola、Daniel Ehrenberg、Jonas Sicking、Don Coleman、Salvatore Iovene、Rijubrata Bhaumik、Wanming Lin、Han Leon、Ryan Sleevi、Balázs Engedy、Theodore Olsauskas-Warren、Reilly Grant、Diego González 和 Daniel Appelquist 对本文档的贡献。
特别感谢 Luc Yriarte 和 Samuel Ortiz 在将 NFC 暴露给 Web 平台方面的初期work,以及他们对当前方法的支持。同时,特别感谢 Elena Reshetova 对“安全与隐私”章节的贡献。