如何为集成 SiriKit 的支付增加 Touch ID 安全验证?
结论
SiriKit 的 Intents 扩展无法可靠地显示 LocalAuthentication 的 Touch ID 或 Face ID 系统界面。扩展在后台运行,没有前台应用界面承载生物识别弹窗。因此,需要验证身份时,Siri 会要求用户“在应用中继续操作”。
建议按以下流程处理:
- Intents 扩展负责解析并校验付款参数。
- 扩展返回
.failureRequiringAppLaunch,通过NSUserActivity将待确认的付款信息交给主应用。 - 主应用进入前台后调用
LAContext.evaluatePolicy。 - 身份验证成功后再提交付款。
为什么当前实现不能正常工作
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 收集付款意图,主应用显示安全验证界面,并在验证成功后执行交易。
备注:内容仅供参考。
