zoukankan      html  css  js  c++  java
  • 你知道怎么使用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

    欢迎与我讨论交流!

    福禄·研发中心 福禄娃
  • 相关阅读:
    SQL Server Audit监控触发器状态
    SQL Server 数据变更时间戳(timestamp)在复制中的运用
    SQL Server 更改跟踪(Chang Tracking)监控表数据
    SQL Server 变更数据捕获(CDC)监控表数据
    SQL Server 事件通知(Event notifications)
    SQL Server 堆表行存储大小(Record Size)
    SQL Server DDL触发器运用
    SQL Server 默认跟踪(Default Trace)
    SQL Server 创建数据库邮件
    SQL Server 跨网段(跨机房)FTP复制
  • 原文地址:https://www.cnblogs.com/fulu/p/15492158.html
Copyright © 2011-2022 走看看