You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 04:20:55