Streamline local authorization flows
System & Services 进阶 20m

简化本地授权流程

Streamline local authorization flows

2022年6月6日

在 Apple 官方观看视频

一句话判断

LocalAuthentication 框架终于引入了”授权”和”认证”的分离——LARight 让你可以用生物识别保护特定操作而不需要每次都走完整认证流程,配合 persisted right 还能在 App 重启后保持授权状态。

这场 Session 讲了什么

这场 Session 讲了 LocalAuthentication 框架在 iOS 16、macOS 13 上的新变化。核心概念是把”认证”(authentication)和”授权”(authorization)区分开。

认证 vs 授权。 之前的 LocalAuthentication 只有 LAContext.evaluatePolicy() 这一种方式——每次需要验证身份时都要走一遍完整的 Face ID / Touch ID 流程。这对”解锁 App”来说没问题,但对于”确认一笔交易”或”删除一个文件”这种场景就有点重了。新的框架引入了”Right”的概念——一个 Right 代表一个受保护的权限或操作,用户通过生物识别获得对这个 Right 的授权。

LARight 和 LAContext 的关系。 LARight 是一个新的类,代表一个需要授权的操作。你可以创建自定义的 Right,也可以用系统预定义的 Right(比如 LAPasswordRight)。Right 的生命周期是:unknown -> authorizing -> authorized / notAuthorized。用户授权后,Right 进入 authorized 状态,你可以在 App 运行期间反复检查这个状态而不需要再次弹出生物识别。

Persisted Right。 这是更高级的特性。LAPersistedRight 可以把授权状态持久化到 Secure Enclave。App 被杀掉重新打开后,如果 Right 仍然在有效期内(你可以设置过期时间),用户不需要再次认证。Secure Enclave 里存的是一个密钥对,私钥永远不离开 Secure Enclave,公钥由你的 App 保存。验证授权状态时,系统用 Secure Enclave 里的私钥做签名验证。

Right 的生命周期管理。 你可以随时 revoke 一个 Right——比如用户点击”锁定”按钮,或者 App 进入后台超过一定时间。Right 被 revoke 后,下次访问需要重新认证。你还可以设置 Right 的约束条件(constraint),比如”只在 Secure Enclave 可用时授权”或者”需要用户在场(user presence)”。

加密操作。 新的 API 还允许你把加密操作绑定到 Right 上。比如,解密一个文件需要先获得 Right 的授权。授权后你可以拿到一个 SecKey,用它来执行签名或解密。密钥的私钥部分存储在 Secure Enclave 里,只有在 Right 处于 authorized 状态时才能使用。

值得深挖的点

Persisted Right 的安全模型。 Persisted Right 的密钥对在 Secure Enclave 里生成,私钥永远不暴露给 App。App 拿到的是公钥引用和 Right 的标识符。验证流程是:App 调用 right.authorize(),Secure Enclave 检查是否满足约束条件(比如用户在场),如果满足就用私钥对 challenge 做签名。App 用公钥验证签名。整个流程不需要网络,完全在设备本地完成。

Right 和 Keychain 的关系。 之前很多开发者用 Keychain 的 kSecAccessControl 来实现”需要 Face ID 才能读取”的逻辑。新的 Right API 是一个更清晰的替代方案——你不再需要往 Keychain 里存一个假密码来做访问控制,直接用 Right 就行。但 Keychain 的 kSecAccessControl 不会被废弃,两者可以共存。

代码片段

创建和使用自定义 Right:

import LocalAuthentication

// 创建一个自定义 Right
let right = LARight()

// 设置约束条件
let constraint = LAConstraint(userPresence: .required)
right.constraint = constraint

// 请求授权
right.authorize { context, error in
    switch right.state {
    case .authorized:
        // 用户已通过 Face ID / Touch ID 验证
        performProtectedAction()
    case .notAuthorized:
        // 用户取消或验证失败
        handleAuthorizationFailure(error)
    default:
        break
    }
}

// 检查授权状态(不会弹出 Face ID)
if right.state == .authorized {
    // 直接执行操作,不需要再次认证
    performProtectedAction()
}

// 撤销授权
right.deauthorize()

使用 Persisted Right 和加密操作:

// 创建持久化 Right
let persistedRight = LAPersistedRight(
    identifier: "com.example.document-access",
    constraint: LAConstraint(userPresence: .required)
)

// 注册 Right(生成密钥对)
persistedRight.register { success, error in
    if success {
        // 获取关联的加密密钥
        let key = persistedRight.key
        // 用 key 做签名或解密操作
    }
}

// App 重启后检查授权状态
if persistedRight.state == .authorized {
    // 不需要重新认证,直接使用
    let key = persistedRight.key
    decryptSensitiveData(with: key)
} else {
    // 需要重新认证
    persistedRight.authorize { context, error in
        // ...
    }
}

最佳实践

为不同的受保护操作创建不同的 Right,而不是用一个全局 Right 管所有事情。比如”查看密码”和”删除账户”应该是两个独立的 Right。这样用户授权”查看密码”后,“删除账户”仍然需要单独授权,安全性更好。

Right 的标识符要稳定——不要用随机 UUID 或可变字符串作为 LAPersistedRight 的 identifier。identifier 变了等于创建了一个新 Right,之前的授权状态会丢失。建议用 bundle ID + 功能描述的格式,比如 com.example.app.payment-authorization

不要在 Right 的 authorize 回调里做耗时操作。回调在主线程执行,如果需要网络请求或大量计算,先保存授权状态,然后在后台执行实际操作。

还有什么值得关注

  • LAContext 的旧 API(evaluatePolicy)完全不受影响,你可以混合使用旧 API 和新 Right API。
  • Persisted Right 在设备重启后需要用户至少解锁一次设备才能使用(和 Keychain 的 kSecAttrAccessibleAfterFirstUnlock 行为一致)。
  • macOS 上 Right 可以绑定到 Apple Watch 解锁,不需要 Touch ID。
  • 如果 Secure Enclave 不可用(比如模拟器),Right 的 authorize 会直接失败。记得在模拟器上测试时做 fallback 处理。
WWDC 2022