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

如何在C#中防止Google Authenticator重复使用过期PIN码

解决Google Authenticator过期PIN重复使用问题

先纠正一个认知偏差

Google Authenticator本质是基于TOTP(时间同步型一次性密码) 标准,它的核心是验证密码是否属于当前或最近的有效时间窗口,而非用PIN来解密字符串(你的业务场景可能是把TOTP作为解密密钥,但重复使用的问题根源在于你的验证/解密逻辑没限制已使用过的密码)。

可行解决方案:跟踪已使用的时间窗口

要强制只能用新PIN,必须记录用户已使用过的有效密码对应的时间窗口——因为TOTP默认每30秒生成一个唯一密码,每个密码对应一个固定的时间窗口ID(通过Unix时间戳 / 30计算)。具体实现步骤:

1. 核心逻辑

  • 验证时,先计算当前及前后1-2个时间窗口的有效PIN(兼容用户设备与服务器的时间偏差)
  • 检查用户输入的PIN是否匹配这些有效窗口
  • 再校验该窗口ID是否已经被使用过,若是则直接拒绝
  • 验证通过后,将该窗口ID标记为已使用

2. C#代码示例(基于Google.Authenticator NuGet包)

// 注意:生产环境要把已使用的窗口ID存在数据库,关联用户ID,不能存在内存
private readonly Dictionary<string, HashSet<long>> _userUsedWindows = new Dictionary<string, HashSet<long>>();
private const int TimeWindowSeconds = 30; // TOTP默认30秒窗口
private const int AllowedPastWindows = 2; // 允许验证最近2个过期窗口(最多90秒前的PIN)

public bool ValidatePinAndPreventReuse(string userId, string inputPin, string totpSecret)
{
    var auth = new TwoFactorAuthenticator();
    long currentWindow = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / TimeWindowSeconds;

    // 检查当前及过去的几个有效窗口
    for (int i = 0; i <= AllowedPastWindows; i++)
    {
        long targetWindow = currentWindow - i;
        string expectedPin = auth.GenerateCode(totpSecret, targetWindow * TimeWindowSeconds);

        if (inputPin != expectedPin) continue;

        // 初始化用户的已使用窗口集合
        if (!_userUsedWindows.ContainsKey(userId))
            _userUsedWindows[userId] = new HashSet<long>();

        // 检查该窗口是否已被使用
        if (_userUsedWindows[userId].Contains(targetWindow))
            return false;

        // 标记窗口为已使用
        _userUsedWindows[userId].Add(targetWindow);
        // 清理过旧的窗口记录,避免冗余存储
        CleanupStaleWindows(_userUsedWindows[userId], currentWindow);
        return true;
    }

    return false; // 输入的PIN无效
}

private void CleanupStaleWindows(HashSet<long> usedWindows, long currentWindow)
{
    // 只保留最近10个窗口的记录,更早的直接删除
    usedWindows.RemoveAll(window => window < currentWindow - 10);
}

3. 关键注意事项

  • 必须持久化存储:内存存储只能用于测试,生产环境要把用户的已使用窗口ID存在数据库(比如给用户表加一个字段存储窗口ID列表,或者单独建关联表),否则服务重启后会丢失记录,导致过期PIN又能被使用
  • 合理设置允许的过期窗口数:不能完全禁止过期窗口,因为用户手机和服务器的时间可能有几十秒的偏差,一般允许1-2个过去的窗口(覆盖60-90秒的误差)
  • 不要存储PIN本身:存储窗口ID比存储PIN更安全,因为PIN是基于窗口和密钥生成的,窗口ID唯一且不会泄露核心密钥

为什么必须做记录

Google Authenticator的客户端和服务端是无状态的,官方库本身不会跟踪哪些PIN已经被使用过——TOTP的“一次性”特性需要服务端自己维护状态来实现,否则只要PIN还在有效时间窗口的计算范围内(甚至过期但没被标记的),就会被重复验证通过。

内容的提问来源于stack exchange,提问作者Programnik

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 22:33:34