现有基础上增加设备生物识别登录的一个技术方案

一、背景

由于登录密码对用户来说并不方便,且有泄露风险。我们可以在现有登录方案:短信验证码/本机号码一键登录/三方人脸识别的基础上,设计一个使用设备生物识别的登录方案,这能进一步提升用户登录便捷性、降低登录成本----短信、本机号码、三方人脸都是要钱的。

二、方案

下面是以前预研的一个基本思路:

核心原则:生物特征只做本地验证,不出设备。 指纹/人脸在 Secure Enclave 里只是"打开保险箱的钥匙",保险箱里存的是你登录所需的凭证。

js 复制代码
┌─────────────────────────────────────────────────────────┐
│              生物识别的本质                               │
│                                                         │
│  不是:  指纹 → 服务器验证 → 登录                           │
│  而是:  指纹 → 解锁本地存储的 Token → 用 Token 登录         │
│                                                        │
│  服务器永远接触不到你的指纹/人脸数据                         │
│  只认 Token,不管 Token 是验证码换的还是生物识别解封的        │
└─────────────────────────────────────────────────────────┘

1. 开启生物识别

已登录后在设置中开启生物识别(指纹/人脸)。

js 复制代码
┌────────── 开启生物识别(设置页操作)───────────────────────┐
│                                                          │
│  前置: 用户已经通过验证码登录,持有有效的 session_token     │
│                                                          │
│  ┌─────────────────────────────────────────────────┐    │
│  │ 用户点击 "开启面容/指纹登录"                       │    │
│  │                                                  │    │
│  │ 客户端做以下检查:                                  │    │
│  │ ┌──────────────────────────────────────────┐    │    │
│  │ │ 1. 检查设备是否支持生物识别                 │    │    │
│  │ │    LAContext.canEvaluatePolicy(.deviceOwner│    │
│  │ │      AuthenticationWithBiometrics)         │    │    │
│  │ │                                           │    │    │
│  │ │ 2. 检查是否已录入指纹/人脸                  │    │    │
│  │ │    biometryType:                           │    │    │
│  │ │      .touchID  /  .faceID  /  .none        │    │    │
│  │ │                                           │    │    │
│  │ │ 3. 如果不支持或未录入 → 提示用户去设置       │    │    │
│  │ │    无法开启此功能                           │    │    │
│  │ └──────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────┘    │
│                                                          │
│  ┌─────────────────────────────────────────────────┐    │
│  │ 检查通过后:                                       │    │
│  │                                                  │    │
│  │ 1. 服务器生成一个 "生物识别凭证" (biometric_token)  │    │
│  │    ───────────────►                               │    │
│  │    POST /api/auth/biometric/enable                │    │
│  │    Header: Authorization Bearer session_token      │    │
│  │    Body: {                                        │    │
│  │      device_id: "a1b2c3...",                      │    │
│  │      biometry_type: "faceID"                      │    │
│  │    }                                              │    │
│  │                                                   │    │
│  │    服务器返回:                                     │    │
│  │    ◄───────────────                                │    │
│  │    {                                              │    │
│  │      biometric_token: "base64url(服务器生成的随机令牌)",│    │
│  │      refresh_token: "base64url(续期用的)",         │    │
│  │      expires_at: 当前+7天                          │    │
│  │    }                                              │    │
│  │                                                  │    │
│  │ 2. 客户端将凭证存入 Keychain                        │    │
│  │    并设置访问控制: 需要生物识别才能读取              │    │
│  │                                                  │    │
│  └─────────────────────────────────────────────────┘    │
│                                                          │
│  ┌─────────────────────────────────────────────────┐    │
│  │ Keychain 存储详情:                                │    │
│  │                                                  │    │
│  │  存取控制标志:                                    │    │
│  │  · .biometryCurrentSet  (当前录入的生物特征)       │    │
│  │  · .privateKeyUsage                               │    │
│  │  · .devicePasscode (备选方案,设备密码兜底)        │    │
│  │                                                  │    │
│  │  存储内容:                                        │    │
│  │  {                                               │    │
│  │    biometric_token,                              │    │
│  │    refresh_token,                               │    │
│  │    expires_at,                                  │    │
│  │    device_id,                                   │    │
│  │    user_id                                      │    │
│  │  }                                               │    │
│  └─────────────────────────────────────────────────┘    │
│                                                          │
└──────────────────────────────────────────────────────────┘

// 一个参考值,可以追加设备名、bundleid等生成
device_id = SHA256(IDFV + 随机种子 + 安装时间戳)

具体代码:

js 复制代码
import Security
import LocalAuthentication

func storeBiometricToken(token: Data, key: String) throws {
    // 访问控制:必须生物认证才能读取
    let accessControl = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        [.biometryCurrentSet, .privateKeyUsage],
        nil
    )!

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: token,
        kSecAttrAccessControl as String: accessControl,
        // 固定在当前设备,备份还原过来也不能用
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]

    // 先删旧值
    SecItemDelete(query as CFDictionary)
    // 写入新值
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw NSError(domain: "KeychainError", code: Int(status))
    }
}

2.生物识别登录流程:

js 复制代码
┌────────── 用户下次打开 App 走生物识别登录 ──────────────────┐
│                                                            │
│  用户打开 App                                               │
│      │                                                     │
│      ▼                                                     │
│  ┌──────────────────────────────────────┐                 │
│  │ 1. 检查本地是否有 biometric_token      │                 │
│  │    · 没有 → 显示验证码登录页(已有)     │                 │
│  │    · 有 → 触发生物识别                 │                 │
│  └──────────────────────────────────────┘                 │
│      │                                                     │
│      ▼                                                     │
│  ┌──────────────────────────────────────┐                 │
│  │ 2. 弹出系统面容/指纹扫描               │                 │
│  │    LAContext.evaluatePolicy(          │                 │
│  │      .deviceOwnerAuthentication       │                 │
│  │        WithBiometrics,                │                 │
│  │      reason: "使用面容登录"            │                 │
│  │    )                                  │                 │
│  └──────────────────────────────────────┘                 │
│      │                                                     │
│      ├── 认证成功 ─────────────────────────────────────────│
│      │                                                     │
│      │  ┌──────────────────────────────────────┐         │
│      │  │ 3. 用生物识别解封 Keychain             │         │
│      │  │    取到 biometric_token + refresh_token│        │
│      │  │    (这个过程对用户透明,系统自动完成)    │         │
│      │  └──────────────────────────────────────┘         │
│      │         │                                           │
│      │         ▼                                           │
│      │  ┌──────────────────────────────────────┐         │
│      │  │ 4. 向服务器发起生物识别登录            │         │
│      │  │    POST /api/auth/biometric/login     │         │
│      │  │    Body: {                            │         │
│      │  │      user_id,                        │         │
│      │  │      device_id,                      │         │
│      │  │      biometric_token                 │         │
│      │  │    }                                 │         │
│      │  └──────────────────────────────────────┘         │
│      │         │                                           │
│      │         ▼                                           │
│      │  ┌──────────────────────────────────────┐         │
│      │  │ 服务器验证:                           │         │
│      │  │ 1. biometric_token 是否有效           │         │
│      │  │ 2. device_id 是否匹配                 │         │
│      │  │ 3. 是否在有效期内                     │         │
│      │  │ 4. 验证通过 → 返回新 session_token    │         │
│      │  │    (同时可能刷新 biometric_token)     │         │
│      │  └──────────────────────────────────────┘         │
│      │         │                                           │
│      │         ▼                                           │
│      │  登录成功 ✅ → 进入 App 主页                         │
│      │                                                     │
│      │  (可选)后台刷新 biometric_token                    │
│      │  POST /api/auth/biometric/refresh                   │
│      │  → 更新 Keychain 中的 biometric_token               │
│      │                                                     │
│      ├── 认证失败 ─────────────────────────────────────────│
│      │     │                                               │
│      │     ▼                                               │
│      │  显示验证码登录页(用户手动输入验证码登录)             │
│      │                                                     │
│      └── 用户选择 "使用其他方式登录" ────────────────────────│
│            │                                               │
│            ▼                                               │
│        回退到验证码登录页                                    │
│                                                            │
└────────────────────────────────────────────────────────────┘

Swift中实现大体代码:

Swift 复制代码
// 大体代码:
func attemptBiometricLogin() {
    let context = LAContext()
    let reason = "确认本人操作,使用面容/指纹登录"

    context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
        if success {
            // 1. 从 Keychain 取出生物识别 Token
            let tokenData = try? KeychainManager.loadBiometricToken()
            
            // 2. 用 Token 向服务器请求正式的 session 凭证
            APIClient.loginWithBiometricToken(tokenData) { result in
                // 处理登录成功或失败(如 Token 过期)
            }
        } else {
            // 用户取消、生物特征不匹配或直接点选"使用其他方式登录"都会进入这里
            // 策略:直接退回验证码登录页,不区分具体错误原因
            DispatchQueue.main.async {
                showSMSLoginScreen()
            }
        }
    }
}

3.核心边界情况处理

情况1:设备未设置人脸/指纹

js 复制代码
┌──────────────────────────────────────────────────┐
│  用户点击 "开启面容登录"                           │
│      │                                           │
│      ▼                                           │
│  检查 LAContext.biometryType:                     │
│      │                                           │
│      ├── .none → 设备没有生物识别硬件(如 iPad 5代)│
│      │   提示: "您的设备不支持面容/指纹"            │
│      │   按钮: "了解详情" (跳转设置)                │
│      │                                           │
│      └── .touchID / .faceID 但未录入               │
│          canEvaluatePolicy 返回错误                │
│          错误码 kLAErrorBiometryNotEnrolled        │
│          提示: "请先在系统设置中录入面容/指纹"       │
│          按钮: "前往设置" → 跳转 Settings           │
│                                                   │
│  UI 处理:                                          │
│  · "开启面容登录"按钮置灰,下方显示原因              │
│  · 或点击后弹 Alert 引导去系统设置                  │
└──────────────────────────────────────────────────┘

情况2:用户更改了人脸/指纹(核心安全机制)

js 复制代码
┌──────────────────────────────────────────────────┐
│  用户去系统设置添加了新面容 / 增删了指纹            │
│      │                                           │
│      ▼                                           │
│  Keychain 中的 biometric_token 的访问控制          │
│  标记了 .biometryCurrentSet                        │
│      │                                           │
│      ▼                                           │
│  生物特征集合发生变化 → 该访问控制立即失效           │
│      │                                           │
│      ▼                                           │
│  下次尝试生物识别登录时:                            │
│  ┌───────────────────────────────────────────┐   │
│  │ LAContext.evaluatePolicy → 认证通过 ✅       │   │
│  │   因为新面容/指纹是合法的                    │   │
│  │                                           │   │
│  │ 但是!Keychain 读取失败 ❌                  │   │
│  │   错误码: errSecAuthFailed                │   │
│  │   原因: .biometryCurrentSet 要求           │   │
│  │   生物特征集合 == 存储时的集合              │   │
│  │   现在变了 → 拒绝访问                      │   │
│  └───────────────────────────────────────────┘   │
│      │                                           │
│      ▼                                           │
│  App 检测到 Keychain 读取失败                      │
│      │                                           │
│      ▼                                           │
│  ┌──────────────────────────────────────┐        │
│  │ 安全的处理策略:                        │        │
│  │                                      │        │
│  │ 1. 清除本地生物识别凭证                │        │
│  │    (删掉失效的 Keychain 条目)          │        │
│  │                                      │        │
│  │ 2. 通知服务器撤销旧 biometric_token    │        │
│  │    POST /api/auth/biometric/revoke   │        │
│  │    Body: { device_id, reason:        │        │
│  │      "biometry_changed" }            │        │
│  │    (防止旧凭证被恶意利用)              │        │
│  │                                      │        │
│  │ 3. 回退到验证码登录                    │        │
│  │    并提示用户生物特征已变更             │        │
│  │    "检测到您的面容/指纹设置有变化,      │        │
│  │     为了安全请重新登录后再次开启"        │        │
│  └──────────────────────────────────────┘        │
│                                                  │
└──────────────────────────────────────────────────┘

情况3:换设备

js 复制代码
┌──────────────────────────────────────────────────┐
│  用户买了新手机,从 iCloud 恢复或迁移数据           │
│      │                                           │
│      ▼                                           │
│  Keychain 数据恢复情况:                            │
│  ┌───────────────────────────────────────────┐   │
│  │ 取决于 kSecAttrAccessible 的设置:           │   │
│  │                                           │   │
│  │ kSecAttrAccessibleWhenUnlocked            │   │
│  │  → 恢复备份会带过来?不一定!              │   │
│  │    取决于备份加密和设备 ID 变化            │   │
│  │                                           │   │
│  │ kSecAttrAccessibleWhenUnlocked            │   │
│  │   ThisDeviceOnly                          │   │
│  │  → 明确告诉系统 "仅限这台设备"             │   │
│  │  → 换设备后此条目不会被恢复 ✅ 安全的       │   │
│  └───────────────────────────────────────────┘   │
│      │                                           │
│      ▼                                           │
│  结论: 方案中使用了 ThisDeviceOnly                  │
│  换设备后:                                         │
│  · Keychain 里没有 biometric_token                │
│  · 旧设备的 biometric_token 已通过 device_id      │
│    绑定,新设备 device_id 不同                     │
│  · 新设备必须走验证码登录(首次安装注册流程)       │
│                                                  │
│  用户登录后可以去设置中重新开启生物识别              │
└──────────────────────────────────────────────────┘

情况4:biometric_token 过期

js 复制代码
┌──────────────────────────────────────────────────┐
│  biometric_token 有效期设计建议: 7天               │
│      │                                           │
│      ▼                                           │
│  连续 7 天未打开 App:                            │
│  → biometric_token 过期                          │
│  → 服务器拒绝: 401 + reason: "biometric_expired"  │
│  → App 回退到验证码登录                           │
│  → 登录成功后自动续期 biometric_token             │
│                                                  │
│  每次成功使用生物识别登录后:                       │
│  → 服务器返回新的 biometric_token                │
│  → App 更新 Keychain,有效期延长 7 天              │
│  → 使用 refresh_token 刷新,无需验证码             │
└──────────────────────────────────────────────────┘

4.关键数据结构

客户端存储

js 复制代码
┌──────────────────────────────────────────────────────┐
│  Keychain (带生物识别保护)                             │
│  ┌────────────────────────────────────────────────┐  │
│  │ biometric_credential: {                         │  │
│  │   biometric_token: "zt7G...",                   │  │
│  │   refresh_token: "r8Xk...",                     │  │
│  │   device_id: "a1b2c3...",                       │  │
│  │   user_id: "zhangsan",                          │  │
│  │   enabled_at: 1700000000                        │  │
│  │ }                                               │  │
│  │                                                 │  │
│  │ 访问控制: .biometryCurrentSet + ThisDeviceOnly    │  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
│  UserDefaults (轻量偏好)                              │
│  ┌────────────────────────────────────────────────┐  │
│  │ biometric_enabled: true/false                   │  │
│  │ last_biometric_login_at: 时间戳                 │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

服务端存储

js 复制代码
┌──────────────────────────────────────────────────────┐
│  用户-设备表 (在现有基础上增加列)                      │
│  user_devices: {                                     │
│    "user_id": {                                      │
│      "device_id_1": {                               │
│        device_name,                                  │
│        biometric_enabled: true,                      │
│        biometric_token_hash: SHA256(zt7G...),        │
│        biometric_enabled_at: 时间戳,                  │
│        biometric_expires_at: 时间戳,                  │
│        ...                                           │
│      }                                               │
│    }                                                 │
│  }                                                    │
│                                                      │
│  说明:                                                │
│  · biometric_token 用 SHA256 哈希存,防泄露          │
│  · 一个 device 最多一个有效的 biometric_token         │
│  · 生物特征变更时, 客户端主动调 revoke 接口            │
│  · 到期自动清理                                      │
└──────────────────────────────────────────────────────┘
相关推荐
黑化旺仔1 小时前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan3 小时前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo3 小时前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记9 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程9 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne15 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记1 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩1 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年1 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008892 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp