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

一、背景

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

二、方案

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

核心原则:生物特征只做本地验证,不出设备。 指纹/人脸在 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 接口            │
│  · 到期自动清理                                      │
└──────────────────────────────────────────────────────┘
相关推荐
嵌入式×边缘AI:打怪升级日志5 小时前
转换模块(十二):实现 RGB 转 RGB + 项目整合与上机实验
开发语言·ios·swift
唐诺5 小时前
IOS学习路线计划
ios
for_ever_love__5 小时前
UI学习:无限轮播视图
学习·ui·ios·objective-c
MonkeyKing6 小时前
iOS Block 底层深度解析:结构、变量捕获、copy逻辑与循环引用本质
ios
MonkeyKing6 小时前
iOS ARC 本质:__strong / __weak / __unsafe_unretained / __autoreleasing 深度解析
ios
humors2217 小时前
全平台日常使用的国外应用
android·ios·app·安卓·应用·国外
pop_xiaoli8 小时前
【iOS】锁的原理
ios·objective-c·cocoa
秋雨梧桐叶落莳8 小时前
iOS——MVC架构学习
学习·ui·ios·架构·mvc·objective-c