注册
iOS

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

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 功能。


76b1ee8e8f3949f3e9ab1aa83cb38e14.png

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
</dict>

设置 Info.plist


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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFCReaderUsageDescription</key>
<string>应用需要您的同意,才能访问 NFC 进行社保卡信息的读写。</string>
</dict>

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A000000632010105</string>
</array>
</dict>


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



导入 CryptoSwift 第三方库


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


adf2ab68391c6df70fbb7a7c228b6127.png



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



代码编程


扩展 NFCISO7816Tag


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

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

import CoreNFC

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

  private var session: NFCTagReaderSession? = nil
  private var sessionContinuation: CheckedContinuation<NFCISO7816Tag, Error>? = 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 编写如下代码所示的页面,包含一个显示卡号的标签和一个读取按钮。

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()
  }
}

实现读取逻辑

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 是卡与读卡器之间传送的信息单元,具体指令描述请参考 wiki.nfc.im/books



运行过程截图


fa6debaee1e5ea63316eae428627d497.png


作者:满天飞舞的蒲公英
链接:https://juejin.cn/post/7213565990055362616
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册