使用 iCloud 钥匙串验证码安全登录
Secure login with iCloud Keychain verification codes
2021年6月10日
一句话判断
iCloud 钥匙串内置了 TOTP 验证码生成器 —— 用户不再需要 Google Authenticator,Safari 在登录时自动从钥匙串填充验证码,这对开发者来说意味着你应该立刻在登录流程中支持 TOTP 二步验证。
这场 Session 讲了什么
iOS 15 和 macOS 12 的 Safari/iCloud Keychain 新增了内置的验证码(Verification Code)管理功能。用户可以在 iCloud Keychain 中存储和生成基于时间的一次性密码(TOTP,Time-based One-Time Password)。
工作流程:网站在设置二步验证时生成一个 TOTP secret(通常以二维码形式展示),用户在 iOS 15 中可以直接用相机扫描二维码添加到钥匙串。之后每次登录时,Safari 会自动在用户名/密码下方的输入框中填充当前有效的 6 位验证码。
对开发者的影响:你的网站需要支持标准的 TOTP 协议(RFC 6238)。设置二维码的格式要遵循 otpauth://totp/ URI scheme,这样 iOS 的相机才能识别并解析。
值得深挖的点
otpauth URI 的格式规范。标准的 TOTP 设置二维码格式为 otpauth://totp/Issuer:Account?secret=XXXX&issuer=IssuerName。iOS 15 的相机识别器要求 issuer 参数必须存在且和标签中的 Issuer 一致。如果你的二维码格式不规范(缺少 issuer 或使用了非标准参数),相机可能无法解析。建议使用 Google Authenticator 兼容的格式。
自动填充的触发条件。Safari 会在检测到 autocomplete="one-time-code" 属性的 <input> 元素时,自动从钥匙串中查找匹配的 TOTP 并填充。匹配规则基于网站的域名和 TOTP 记录的 issuer。如果你的登录表单没有这个属性,Safari 也会尝试通过启发式方法识别验证码输入框,但显式声明更可靠。
代码片段
Web 端生成 TOTP 设置二维码:
<!-- 登录表单中的验证码输入框 -->
<form action="/login" method="POST">
<input type="text" name="username" autocomplete="username">
<input type="password" name="password" autocomplete="current-password">
<!-- 关键:使用 one-time-code 自动填充类型 -->
<input type="text" name="code" inputmode="numeric"
autocomplete="one-time-code"
pattern="[0-9]*" maxlength="6"
placeholder="验证码">
<button type="submit">登录</button>
</form>
服务端生成 TOTP Secret:
import CryptoKit
import Foundation
func generateTOTPSetupURI(account: String, issuer: String, secret: String) -> String {
// 生成标准 otpauth URI
let encodedIssuer = issuer.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? issuer
let encodedAccount = account.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? account
return "otpauth://totp/\(encodedIssuer):\(encodedAccount)?secret=\(secret)&issuer=\(encodedIssuer)"
}
// 生成二维码供用户扫描
func generateTOTPQRCode(account: String, issuer: String) {
let secret = generateRandomSecret() // Base32 编码的随机密钥
let uri = generateTOTPSetupURI(account: account, issuer: issuer, secret: secret)
// 将 URI 编码为二维码展示给用户
let qrCodeImage = generateQRCode(from: uri)
// 同时在服务端存储 secret 和用户的关联
storeTOTPSecret(secret, for: account)
}
验证 TOTP 验证码:
import CryptoKit
import Foundation
func verifyTOTP(code: String, secret: String) -> Bool {
// 获取当前时间步(30 秒为一个时间窗口)
let timeStep = Int(Date().timeIntervalSince1970 / 30)
// 将时间步转为大端序的 8 字节数据
var timeBytes = withUnsafeBytes(of: timeStep.bigEndian) { Data($0) }
// 用 HMAC-SHA1 计算哈希
let keyData = base32Decode(secret)!
let hmac = HMAC<Insecure.SHA1>.authenticationCode(
for: timeBytes,
using: SymmetricKey(data: keyData)
)
let hash = Data(hmac)
// 动态截取
let offset = Int(hash.last! & 0x0F)
let codeValue = (Int(hash[offset]) & 0x7F) << 24
| (Int(hash[offset + 1]) & 0xFF) << 16
| (Int(hash[offset + 2]) & 0xFF) << 8
| (Int(hash[offset + 3]) & 0xFF)
let totp = String(format: "%06d", codeValue % 1000000)
return totp == code
}
最佳实践
- 登录页面的验证码输入框必须加
autocomplete="one-time-code"。这一行 HTML 就能让 Safari 自动填充,用户体验从”打开验证器 app -> 复制 -> 切回浏览器 -> 粘贴”变成”什么都不用做”。 - TOTP Secret 只展示一次。用户添加到钥匙串后,服务端应该标记设置完成,不再显示二维码或 Secret。如果用户丢失了钥匙串访问,通过备选验证方式(邮箱/手机号)重新设置。
- 支持备选验证方式。不是所有用户都用 iCloud Keychain,保留短信、邮件、Authenticator app 等备选方案。
还有什么值得关注
- iCloud Keychain 的验证码在所有登录同一 Apple ID 的设备间同步(端到端加密)。
- macOS 上的 Safari 也支持同样的自动填充行为。
- 如果你的 app 使用 WebViews 登录,
WKWebView在 iOS 15 中也支持验证码自动填充。