• 你知道怎么使用Google两步验证保护账户安全吗?


    为什么我们需要使用它?

    互联网是一个极其危险的地方,有很多不怀好意的人想要访问我们的在线账户。通过使用双因素身份验证,可以为我们的账号提供额外的安全。用户名密码方式的登录变得越来越不安全,你肯定听说过“撞库”这个名词,是黑客圈的术语,即网络黑客将互联网上已泄露的账号密码,拿到其他网站批量登录,从而“撞出”其他网站的账号密码。很不幸的是,由于许多网民习惯多个网站使用一个账号密码,所以“撞库”有着不低的成功率。

    b7003af33a87e950c595a81310385343faf2b4c11

    对有些人来说,盗取密码比您想象的更简单

    以下任意一种常见操作都可能让您面临密码被盗的风险:

    • 在多个网站上使用同一密码
    • 从互联网上下载软件
    • 点击电子邮件中的链接
      两步验证可以将别有用心的人阻挡在外,即使他们知道您的密码也无可奈何。

    什么是Google两步验证?

    借助Google两步验证,通过密码和手机为帐户提供双重保护

    b7003af33a87e950c595a81310385343faf2b4c12
    Q

    第一步:您需要输入密码

    每当您登录账户时,都需要照常输入账号密码。

    第二步:还需要执行其他操作

    接着,验证码将会以短信的形式发送到手机上或通过语音电话告知,或者通过Google Authenticator App生成提供。

    多一道安全防线

    大多数用户的帐户只有密码这一道安全防线。启用两步验证后,即使有人破解了您的密码,他们仍需要借助您的手机或安全密钥,才能登录您的帐户。

    b7003af33a87e950c595a81310385343faf2b4c13

    什么是Google Authenticator ?

    Google Authenticator(Wiki)是谷歌推出的基于时间的动态口令app(谷歌身份验证),只需要在手机上安装该APP,就可以生成一个随着时间变化的一次性口令,解决大家的账户遭到恶意攻击的问题,在手机端生成动态口令后,除了用正常用户名和密码外,需要输入一次动态口令才能验证成功。

    Google Authenticator采用的算法是TOTP(Time-Based One-Time Password基于时间的一次性密码),其核心内容包括以下三点:

    • 一个共享密钥(一个字节序列);
    • 当前时间输入;
    • 一个签名函数。

    具体原理推荐大家阅读:

    Google账户两步验证的工作原理

    详解Google Authenticator工作原理

    我在这里准备了一个完整可执行的C# WinForm程序,感兴趣的朋友请 点击这里 进行查看,提取码:hemd。

    Dingtalk_202110311327026

    Dingtalk_20211031134446

    • Account Name:对应我们的账号名,可以是手机号、邮箱等
    • Secret Key:这个是我们的密钥Key,用于生成密钥。一般我们将这个值存放在用户表中的某个字段中。
    • Encoded Key:这个是最终生成的密钥,用户如果无法扫码二维码,我们可以将密钥发送至用户手机。

    上图中,可以看出有3个口令,即我们在代码中设置的漂移为30s,主要是防止出现如下问题:

    • 由于网络延时,用户输入延迟等因素,可能当服务器端接收到一次性密码时,T的数值已经改变,这样就会导致服务器计算的一次性密码值与用户输入的不同,验证失败。解决这个问题个一个方法是,服务器计算当前时间片以及前面的n个时间片内的一次性密码值,只要其中有一个与用户输入的密码相同,则验证通过。当然,n不能太大,否则会降低安全性。
    • 我们知道如果客户端和服务器的时钟有偏差,会造成与上面类似的问题,也就是客户端生成的密码和服务端生成的密码不一致。但是,如果服务器通过计算前n个时间片的密码并且成功验证之后,服务器就知道了客户端的时钟偏差。因此,下一次验证时,服务器就可以直接将偏差考虑在内进行计算,而不需要进行n次计算。

    下面是C#源码提供:

    public class TwoFactorAuthenticator
    {
        private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        private TimeSpan DefaultClockDriftTolerance { get; set; }
    
        public TwoFactorAuthenticator()
        {
            DefaultClockDriftTolerance = TimeSpan.FromSeconds(30);  //建议此处将时间漂移设置为30s,即允许前后各一个时间片。不能不建议设置太大,否则会降低安全性
        }
    
        public TwoFactorAuthenticator(TimeSpan defaultClockDriftTolerance)
        {
            DefaultClockDriftTolerance = defaultClockDriftTolerance;
        }
    
        /// <summary>
        /// Generate a setup code for a Google Authenticator user to scan
        /// </summary>
        /// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param>
        /// <param name="accountName">Account Name (no spaces)</param>
        /// <param name="accountSecretKey">Account Secret Key</param>
        /// <param name="qrPixelsPerModule">Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode)</param>
        /// <returns>SetupCode object</returns>
        public SetupCode GenerateSetupCode(string issuer, string accountName, string accountSecretKey, int qrPixelsPerModule)
        {
            var key = Encoding.UTF8.GetBytes(accountSecretKey);
            return GenerateSetupCode(issuer, accountName, key, qrPixelsPerModule);
        }
    
        /// <summary>
        /// Generate a setup code for a Google Authenticator user to scan
        /// </summary>
        /// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param>
        /// <param name="accountName">Account Name (no spaces)</param>
        /// <param name="accountSecretKey">Account Secret Key as byte[]</param>
        /// <param name="qrPixelsPerModule">Number of pixels per QR Module (2 = ~120x120px QRCode)</param>
        /// <returns>SetupCode object</returns>
        public SetupCode GenerateSetupCode(string issuer, string accountName, byte[] accountSecretKey, int qrPixelsPerModule)
        {
            if (accountName == null) { throw new NullReferenceException("Account Title is null"); }
            accountName = accountName.Trim();
            var encodedSecretKey = Base32Encode(accountSecretKey);
            var provisionUrl = string.IsNullOrWhiteSpace(issuer) ? $"otpauth://totp/{accountName}?secret={encodedSecretKey}" : string.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", accountName, encodedSecretKey, HttpUtility.UrlEncode(issuer, Encoding.UTF8));
            using (var qrGenerator = new QRCodeGenerator())
            using (var qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.Q))
            using (var qrCode = new QRCode(qrCodeData))
            using (var qrCodeImage = qrCode.GetGraphic(qrPixelsPerModule))
            using (var ms = new MemoryStream())
            {
                qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
                return new SetupCode(accountName, encodedSecretKey, Convert.ToBase64String(ms.ToArray()));
            }
        }
    
        public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6)
        {
            return GenerateHashedCode(accountSecretKey, counter, digits);
        }
    
        internal string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
        {
            var key = Encoding.UTF8.GetBytes(secret);
            return GenerateHashedCode(key, iterationNumber, digits);
        }
    
        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <param name="iterationNumber"></param>
        /// <param name="digits">The digits parameter may have the values 6 or 8, and determines how long of a one-time passcode to display to the user. The default is 6.</param>
        /// <returns></returns>
        internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6)
        {
            var counter = BitConverter.GetBytes(iterationNumber);
    
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(counter);
            }
    
            var hmac = new HMACSHA1(key);
    
            var hash = hmac.ComputeHash(counter);
    
            var offset = hash[hash.Length - 1] & 0xf;
    
            // Convert the 4 bytes into an integer, ignoring the sign.
            var binary =
                ((hash[offset] & 0x7f) << 24)
                | (hash[offset + 1] << 16)
                | (hash[offset + 2] << 8)
                | (hash[offset + 3]);
    
            var password = binary % (int)Math.Pow(10, digits);
            return password.ToString(new string('0', digits));
        }
    
        private long GetCurrentCounter()
        {
            return GetCurrentCounter(DateTime.UtcNow, _epoch, 30);
        }
    
        private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep)
        {
            return (long)(now - epoch).TotalSeconds / timeStep;
        }
    
        public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient)
        {
            return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance);
        }
    
        public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance)
        {
            var codes = GetCurrentPINs(accountSecretKey, timeTolerance);
            return codes.Any(c => c == twoFactorCodeFromClient);
        }
    
        public string GetCurrentPIN(string accountSecretKey)
        {
            return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter());
        }
    
        public string GetCurrentPIN(string accountSecretKey, DateTime now)
        {
            return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30));
        }
    
        public string[] GetCurrentPINs(string accountSecretKey)
        {
            return GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance);
        }
    
        public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance)
        {
            var codes = new List<string>();
            var iterationCounter = GetCurrentCounter();
            var iterationOffset = 0;
    
            if (timeTolerance.TotalSeconds >= 30)
            {
                iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00);
            }
    
            var iterationStart = iterationCounter - iterationOffset;
            var iterationEnd = iterationCounter + iterationOffset;
    
            for (var counter = iterationStart; counter <= iterationEnd; counter++)
            {
                codes.Add(GeneratePINAtInterval(accountSecretKey, counter));
            }
    
            return codes.ToArray();
        }
    
        private string Base32Encode(byte[] data)
        {
            const int inByteSize = 8;
            const int outByteSize = 5;
            var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
    
            int i = 0, index = 0;
            var result = new StringBuilder((data.Length + 7) * inByteSize / outByteSize);
    
            while (i < data.Length)
            {
                var currentByte = data[i];
    
                /* Is the current digit going to span a byte boundary? */
                int digit;
                if (index > (inByteSize - outByteSize))
                {
                    var nextByte = (i + 1) < data.Length ? data[i + 1] : 0;
    
                    digit = currentByte & (0xFF >> index);
                    index = (index + outByteSize) % inByteSize;
                    digit <<= index;
                    digit |= nextByte >> (inByteSize - index);
                    i++;
                }
                else
                {
                    digit = (currentByte >> (inByteSize - (index + outByteSize))) & 0x1F;
                    index = (index + outByteSize) % inByteSize;
                    if (index == 0)
                        i++;
                }
                result.Append(alphabet[digit]);
            }
    
            return result.ToString();
        }
    }
    
    

    示例程序代码:

    public partial class FrmMain : Form
    {
        public FrmMain()
        {
            InitializeComponent();
        }
    
        private void FrmMain_Load(object sender, EventArgs e)
        {
    
        }
    
        private void btnSetup_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(txtSecretKey.Text.Trim()) || string.IsNullOrEmpty(txtSecretKey.Text.Trim()) || string.IsNullOrEmpty(txtAccountName.Text.Trim())) return;
            var tfA = new TwoFactorAuthenticator();
            var setupCode = tfA.GenerateSetupCode(txtIssuer.Text.Trim(), txtAccountName.Text.Trim(), this.txtSecretKey.Text.Trim(), 3);
            var ms = new MemoryStream(Convert.FromBase64String(setupCode.QrCodeSetupImageUrl));
            this.pbQR.Image = Image.FromStream(ms);
            ms.Dispose();
            this.txtSetupCode.Text = $@"Account: {setupCode.Account}{System.Environment.NewLine}Secret Key: {this.txtSecretKey.Text.Trim()}{System.Environment.NewLine}Encoded Key: {setupCode.ManualEntryKey}";
        }
    
        private void btnGetCurrentCode_Click(object sender, EventArgs e)
        {
            this.txtCurrentCodes.Text = string.Join(System.Environment.NewLine, new TwoFactorAuthenticator().GetCurrentPINs(this.txtSecretKey.Text));
        }
    
        private void btnTest_Click(object sender, EventArgs e)
        {
            var tfA = new TwoFactorAuthenticator();
            var result = tfA.ValidateTwoFactorPIN(txtSecretKey.Text, this.txtCode.Text);
            MessageBox.Show(result ? "Validated" : "Incorrect", "Result");
        }
    }
    

    使用Google两步验证的好处

    • 接入使用简单,门槛低,零成本
    • 保护系统账户安全
    • 节省企业成本(如短信、邮件需要额外费用)
    • 只需一部手机,同时管理多个账户

    实际项目效果演示

    sdd

    欢迎与我讨论交流!

    福禄·研发中心 福禄娃
  • 相关阅读:
    质数检测器
    《视觉SLAM十四讲》学习日志(一)——预备知识
    C++类的介绍
    Python数据类型解析(基础篇)
    括号匹配详解
    哈夫曼树编码
    分治之归并,快速排序
    洛谷p2216 多次单调队列,扫描矩阵中的最大值减去最小值最的固定大小子矩阵
    洛谷p1886滑动窗口最大最小值 双单调队列
    洛谷p1725 露琪诺 单调队列优化的DP
  • 原文地址:https://www.cnblogs.com/fulu/p/15492158.html
Copyright © 2020-2023  润新知