iOS 使用 CoreNFC 读取第三代社保卡信息

语言: CN / TW / HK

theme: cyanosis highlight: atom-one-dark


NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。

iOS 和 watchOS 设备内置 NFC 硬件已经很多年了。在现实生活中,Apple Pay 就是使用这项技术与商店的支付终端进行交互。然而直到 iOS 11 开发者才能够使用 NFC 硬件。后来 Apple 在 iOS 13 系统中提升了 CoreNFC 的功能,开发者可以借助这项新技术,对 iOS 设备进行编程,使其以新的方式与周围的互联世界进行交互。

说明:本文提供的代码示例所用的开发环境为 Xcode14 + Swift 5.7 + iOS 13。需要登录已付费的开发者账号才能开启 NFC Capability。

工程配置

设置 Capability

在项目导航器中选中项目,转到 Signing & Capabilities 标签页并选择 +Capability,在弹出的列表中选择 Near Field Communication Tag Reading。这会自动生成 entitlements 文件中的必要配置信息,同时为您的应用程序激活 NFC 功能。

add-capability.png

```xml

com.apple.developer.nfc.readersession.formats TAG ```

设置 Info.plist

添加 NFC 相关的隐私设置,向 Info.plist 文件中添加 Privacy - NFC Scan Usage Description 隐私设置项。

```xml

NFCReaderUsageDescription 应用需要您的同意,才能访问 NFC 进行社保卡信息的读写。 ```

添加 AID 相关的设置项,向 Info.plist 文件中添加 ISO7816 application identifiers for NFC Tag Reader Session 配置项。

```xml

com.apple.developer.nfc.readersession.iso7816.select-identifiers A000000632010105 ```

说明:第三代社保卡使用统一的交通联合卡电子钱包规范,A000000632010105 为交通联合卡 AID 标识。参考网址:http://wiki.nfc.im/books 。

导入 CryptoSwift 第三方库

在项目导航器中选中项目,右键菜单选择 Add Packages...,在搜索框中输入 http://github.com/krzyzanowskim/CryptoSwift 并点击 Add Package 按钮完成导入。

add-package.png

说明:CryptoSwift 提供了相关的十六进制字符串与 UInt8 相互转换的方法。

代码编程

扩展 NFCISO7816Tag

由于 Apple 是从 iOS 14 系统开始提供了 sendCommand API 的异步调用形式,为兼容 iOS 13 系统,并更好的使用 Swift 提供的 async/await 语法,现对其 NFCISO7816Tag 进行方法扩展。

```swift import CoreNFC import CryptoSwift

@available(iOS 13.0, *) extension NFCISO7816Tag {

@discardableResult   func sendCommand( command: String) async throws -> Data {     return try await withCheckedThrowingContinuation { continuation in       // 通过 CryptoSwift 库提供的 API,将十六进制表示命令字符串转换成字节       let apdu = NFCISO7816APDU(data: Data(hex: command))!       // 将同步调用形式转换成异步调用形式       sendCommand(apdu: apdu) { responseData, , _, error in         if let error {           continuation.resume(throwing: error)         } else {           continuation.resume(returning: responseData)         }       }     }   } } ```

封装 NFCTagReaderSession

```swift import CoreNFC

@available(iOS 13.0, *) class NFCISO7816TagSession: NSObject, NFCTagReaderSessionDelegate {

private var session: NFCTagReaderSession? = nil   private var sessionContinuation: CheckedContinuation? = nil

func begin() async throws -> NFCISO7816Tag { // 实例化用于检测 NFCISO7816Tag 的会话     session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)     session?.alertMessage = "请将社保卡靠近手机背面上方的 NFC 感应区域"     session?.begin()     return try await withCheckedThrowingContinuation { continuation in       self.sessionContinuation = continuation     }   }

func invalidate(with message: String) { // 关闭读取会话,以防止重用     session?.alertMessage = message     session?.invalidate()   }

// MARK: - NFCTagReaderSessionDelegate

func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { // 检测到 NFCISO7816Tag     if let tag = tags.first, case .iso7816(let iso7816Tag) = tag {       session.alertMessage = "正在读取信息,请勿移动社保卡" // 连接到 NFCISO7816Tag 并将同步调用形式转换成异步调用形式       session.connect(to: tag) { error in         if let error {           self.sessionContinuation?.resume(throwing: error)         } else {           self.sessionContinuation?.resume(returning: iso7816Tag)         }       }     }   }

func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { // 读取过程中发生错误     self.session = nil     sessionContinuation?.resume(throwing: error)   } } ```

编写 UI 界面

使用 SwiftUI 编写如下代码所示的页面,包含一个显示卡号的标签和一个读取按钮。

```swift import SwiftUI

struct ContentView: View {   @State private var cardNo = ""

var body: some View {     VStack(alignment: .leading) {       Text("卡号:(cardNo)")         .font(.system(size: 17))       Button(action: read) {         Text("读取")           .padding()           .frame(maxWidth: .infinity)           .foregroundColor(.white)           .background(.blue)           .cornerRadius(8)       }       Spacer()     }     .padding()   } } ```

实现读取逻辑

```swift import SwiftUI import CryptoSwift

struct ContentView: View { // var body: some View {...}

private func read() {     Task {       let session = NFCISO7816TagSession()       do { // 检测 NFCISO7816Tag         let tag = try await session.begin() // 发送命令 00B0950A12 并截取前 10 个字节转换为 20 位卡号         let cardNo = try await tag.sendCommand("00B0950A12")[0..<10].toHexString()         self.cardNo = cardNo // 关闭读取会话         session.invalidate(with: "读取成功")       } catch {         print(error)       }     }   } } ```

说明:APDU 是卡与读卡器之间传送的信息单元,具体指令描述请参考 http://wiki.nfc.im/books 。

运行过程截图

screenshot.png