PAYATHON 2026

如何为集成 SiriKit 的支付增加 Touch ID 安全验证?

支付小周

结论

SiriKit 的 Intents 扩展无法可靠地显示 LocalAuthentication 的 Touch ID 或 Face ID 系统界面。扩展在后台运行,没有前台应用界面承载生物识别弹窗。因此,需要验证身份时,Siri 会要求用户“在应用中继续操作”。

建议按以下流程处理:

  1. Intents 扩展负责解析并校验付款参数。
  2. 扩展返回 .failureRequiringAppLaunch,通过 NSUserActivity 将待确认的付款信息交给主应用。
  3. 主应用进入前台后调用 LAContext.evaluatePolicy。
  4. 身份验证成功后再提交付款。

为什么当前实现不能正常工作

Intents 扩展不适合发起交互式身份验证

INSendPaymentIntentHandling 通常在 Intents 扩展中执行。扩展没有自己的前台界面,也不能假定系统会允许它弹出 Touch ID 或 Face ID 提示。

LAContext.evaluatePolicy 需要与用户交互。即使 canEvaluatePolicy 返回 true,也只能说明设备支持相应能力,并且当前策略可用,不能说明 Intents 扩展具备显示验证界面的条件。

Siri 将流程交给主应用,是正常的安全处理方式,并不表示付款逻辑出了问题。

示例代码提前放行了付款

代码末尾的这行必须删除:

successAuth()

evaluatePolicy 是异步调用。原代码没有等待验证结果,便直接执行了 successAuth()。这可能造成以下问题:

  • 用户还没有完成验证,付款逻辑已经开始执行;
  • 验证成功后,successAuth() 可能再次执行;
  • completion 可能被多次调用;
  • 可能出现重复付款或扩展生命周期异常等严重问题。

不过,删除这行代码仍然无法让扩展显示认证界面。身份验证应放到主应用中完成。

推荐的实现流程

1. Intents 扩展只准备付款请求

扩展负责检查收款人、金额和币种等参数,然后构造 NSUserActivity,要求启动主应用继续处理:

func handle(
    sendPayment intent: INSendPaymentIntent,
    completion: @escaping (INSendPaymentIntentResponse) -> Void
) {
    guard
        let payee = intent.payee,
        let payeeHandle = payee.personHandle,
        let amount = intent.currencyAmount?.amount,
        let currencyCode = intent.currencyAmount?.currencyCode
    else {
        completion(
            INSendPaymentIntentResponse(
                code: .failure,
                userActivity: nil
            )
        )
        return
    }

    let activity = NSUserActivity(
        activityType: "com.example.payment.confirm-send-payment"
    )

    activity.title = "Confirm payment"
    activity.userInfo = [
        "payeeHandle": payeeHandle.value,
        "amount": amount.stringValue,
        "currencyCode": currencyCode
    ]

    completion(
        INSendPaymentIntentResponse(
            code: .failureRequiringAppLaunch,
            userActivity: activity
        )
    )
}

这里不要调用 paymentProvider.send(...)。扩展只负责把待确认交易交给主应用,实际付款必须等身份验证成功后才能执行。

在生产环境中,不建议只通过 userInfo 传递完整且可直接执行的支付指令。更稳妥的做法是生成一个短期有效、只能使用一次的交易 ID,再由主应用从安全存储或后端重新获取并校验交易信息。

2. 主应用接收继续操作请求

主应用收到 NSUserActivity 后,解析待确认的付款信息,并显示确认页面。具体可以在 AppDelegate、SceneDelegate 或相应的 SwiftUI 入口中处理,取决于应用采用的生命周期架构。

以下是 SceneDelegate 的示例:

func scene(
    _ scene: UIScene,
    continue userActivity: NSUserActivity
) {
    guard
        userActivity.activityType ==
            "com.example.payment.confirm-send-payment",
        let info = userActivity.userInfo,
        let payeeHandle = info["payeeHandle"] as? String,
        let amountString = info["amount"] as? String,
        let currencyCode = info["currencyCode"] as? String
    else {
        return
    }

    presentPaymentConfirmation(
        payeeHandle: payeeHandle,
        amountString: amountString,
        currencyCode: currencyCode
    )
}

接收入口会受到 UIKit 或 SwiftUI 生命周期以及最低系统版本的影响,但处理原则不变:主应用进入前台后,必须重新验证所有交易参数。

3. 在主应用中执行身份验证

主应用进入前台并拥有可用界面后,再调用 LocalAuthentication:

import LocalAuthentication

func authenticateForPayment(
    completion: @escaping (Result<Void, Error>) -> Void
) {
    let context = LAContext()
    var evaluationError: NSError?

    let policy: LAPolicy = .deviceOwnerAuthentication

    guard context.canEvaluatePolicy(
        policy,
        error: &evaluationError
    ) else {
        completion(
            .failure(
                evaluationError ?? AuthenticationError.unavailable
            )
        )
        return
    }

    context.evaluatePolicy(
        policy,
        localizedReason: "验证身份以确认付款"
    ) { success, error in
        DispatchQueue.main.async {
            if success {
                completion(.success(()))
            } else {
                completion(
                    .failure(error ?? AuthenticationError.failed)
                )
            }
        }
    }
}

enum AuthenticationError: Error {
    case unavailable
    case failed
}

付款确认页面调用该方法,并且只在验证成功后提交交易:

authenticateForPayment { [weak self] result in
    switch result {
    case .success:
        self?.submitVerifiedPayment()

    case .failure(let error):
        self?.showAuthenticationError(error)
    }
}

使用 .deviceOwnerAuthentication 时,如果生物识别不可用或连续失败,系统可以回退到设备密码。业务如果明确要求只能使用生物识别,可以改用:

.deviceOwnerAuthenticationWithBiometrics

这种情况下,应用需要自行处理未录入生物识别、生物识别被锁定或不可用等情况。

注意事项

  • 身份验证前不要创建不可撤销的付款。此时最多只能创建待确认订单或一次性交易令牌。
  • 主应用收到 NSUserActivity 后,必须重新校验收款人、金额、币种、交易状态和令牌有效期,不能直接信任扩展传来的字典内容。
  • 应防止重复提交。身份验证回调、应用恢复和网络重试都可能重复触发业务逻辑,服务端应使用幂等键或一次性交易 ID。
  • evaluatePolicy 的回调不保证在主线程执行。更新界面前应切回主线程。
  • 不要强制解包 error!,因为某些失败路径中的错误对象可能为空。
  • 支持 Face ID 的应用需要在 Info.plist 中配置 NSFaceIDUsageDescription。
  • Siri 自身的设备解锁状态、语音确认或意图确认,不能自动代替应用要求的交易级身份验证。

如果付款必须经过 Touch ID、Face ID 或设备密码验证,“在应用中继续”就是合适的处理方式。Siri 收集付款意图,主应用显示安全验证界面,并在验证成功后执行交易。

备注:内容仅供参考。