iOS端TOTP与Google Authenticator等应用同步及实现问题咨询
解决TOTP同步与UI刷新问题的方案
嘿,我来帮你搞定这个TOTP同步和UI混乱的问题!咱们分两部分来拆解:先解决和Google Authenticator不同步的核心问题,再优化UI刷新逻辑,避免影响进度视图和编辑功能。
一、TOTP不同步的核心原因与解决方法
TOTP的同步完全依赖标准的时间计算逻辑,Google Authenticator严格遵循RFC 6238规范,所以咱们得确保自己的实现完全对齐:
1. 用UTC时间戳计算计数器(Counter)
TOTP的核心是Counter = floor(当前Unix时间戳 / 步长),这里的步长是30秒。必须用UTC时间戳(也就是Date().timeIntervalSince1970,这个本身就是UTC时间),绝对不能用本地时间(比如考虑时区偏移的时间),否则一旦用户修改本地时区或时间,就会完全不同步。
正确的Counter计算代码:
let step: TimeInterval = 30 let counter = UInt64(floor(Date().timeIntervalSince1970 / step))
2. 确保密钥解码正确
二维码中的TOTP密钥几乎都是Base32编码的,解码时要注意:
- Base32只允许大写字母A-Z、数字2-7,还有可选的填充符
=,如果你的密钥是小写,要先转成大写再解码。 - 用靠谱的Base32解码逻辑(可以自己实现或用成熟的工具类),解码后的二进制数据错误会直接导致OTP不匹配。
3. 验证HMAC算法实现
TOTP默认用HMAC-SHA1,要确保你的HMAC计算完全符合规范。推荐用系统的CommonCrypto框架实现,避免第三方库的差异。
二、优化UI刷新逻辑,避免干扰进度视图和编辑功能
每秒刷新整个TableView绝对是大忌——不仅会导致单元格闪烁、进度视图重置,还会干扰编辑/删除操作。咱们换个思路:只刷新需要更新的部分,并且区分进度刷新和OTP更新的时机。
1. 拆分计时器任务
- 进度视图:每秒更新一次,但只更新当前可见的单元格,不用刷新整个TableView。
- OTP更新:每30秒才需要更新一次,同样只刷新可见单元格的OTP标签,而非整个列表。
2. 实现代码示例
class OTPViewController: UITableViewController { var secretArray: [Data] = [] // 存储解码后的密钥 var totpArray: [String] = [] // 存储生成的OTP var endTime: TimeInterval = 0 // 当前30秒周期的结束时间 var timer: Timer? override func viewDidLoad() { super.viewDidLoad() setupTimer() updateAllTOTPs() // 初始化生成所有OTP } // 初始化计时器和周期结束时间 func setupTimer() { updateEndTime() timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in guard let self = self else { return } let currentTime = Date().timeIntervalSince1970 let remainingTime = self.endTime - currentTime // 当周期结束时,更新所有OTP并重置周期 if remainingTime <= 0 { self.updateAllTOTPs() self.updateEndTime() // 刷新可见单元格的OTP标签 self.tableView.indexPathsForVisibleRows?.forEach { indexPath in if let cell = self.tableView.cellForRow(at: indexPath) as? OTPTableViewCell { cell.otpLabel.text = self.totpArray[indexPath.row] } } } // 更新可见单元格的进度视图 self.tableView.visibleCells.forEach { cell in guard let otpCell = cell as? OTPTableViewCell else { return } // 进度值范围是0-1,剩余时间/30就是当前进度 otpCell.progressView.progress = Float(max(remainingTime, 0) / 30) } } } // 计算当前周期的结束时间 func updateEndTime() { let currentTime = Date().timeIntervalSince1970 let remainder = currentTime.truncatingRemainder(dividingBy: 30) endTime = currentTime + (30 - remainder) } // 重新生成所有TOTP func updateAllTOTPs() { totpArray = secretArray.map { secret in self.generateTOTP(for: secret) } } // 标准TOTP生成逻辑 func generateTOTP(for secret: Data, step: TimeInterval = 30, digits: Int = 6) -> String { let counter = UInt64(floor(Date().timeIntervalSince1970 / step)) var counterBytes = [UInt8](repeating: 0, count: 8) // 将Counter转成大端字节序 for i in 0..<8 { counterBytes[7 - i] = UInt8((counter >> (i * 8)) & 0xFF) } // HMAC-SHA1计算 var hmacResult = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), secret.bytes, secret.count, counterBytes, counterBytes.count, &hmacResult) // 动态截断获取OTP let offset = hmacResult.last! & 0x0F let truncatedValue = (UInt32(hmacResult[offset] & 0x7F) << 24) | (UInt32(hmacResult[offset+1]) << 16) | (UInt32(hmacResult[offset+2]) << 8) | UInt32(hmacResult[offset+3]) let otp = truncatedValue % UInt32(pow(10, Double(digits))) return String(format: "%0\(digits)d", otp) } // 编辑模式下暂停计时器,避免干扰操作 override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) if editing { timer?.invalidate() } else { setupTimer() } } }
3. 额外注意事项
- 密钥安全存储:绝对不能把TOTP密钥存在
UserDefaults里,要用iOS的Keychain存储敏感信息,防止泄露。 - 测试同步性:可以手动对比Google Authenticator和你的应用生成的OTP,同时用UTC时间戳验证计数器计算是否正确。
- 处理后台情况:如果应用进入后台,计时器会暂停,回到前台时要重新计算
endTime,避免进度和OTP不同步。
内容的提问来源于stack exchange,提问作者Purna chandra
相关产品推荐
相关产品推荐

