为什么我们需要使用它?

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

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

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

  • 在多个网站上使用同一密码
  • 从互联网上下载软件
  • 点击电子邮件中的链接

    两步验证可以将别有用心的人阻挡在外,即使他们知道您的密码也无可奈何。

什么是Google两步验证?

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



Q

第一步:您需要输入密码

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

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

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

多一道安全防线

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

什么是Google Authenticator ?

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

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

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

具体原理推荐大家阅读:

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

详解Google Authenticator工作原理

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

  • 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两步验证的好处

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

实际项目效果演示

欢迎与我讨论交流!

福禄·研发中心
福禄娃

你知道怎么使用Google两步验证保护账户安全吗?的更多相关文章

  1. 为Linux服务器的SSH登录启用Google两步验证

    对于Linux服务器而言使用密钥登录要比使用密码登录安全的多,毕竟当前网上存在多个脚本到处进行爆破. 这类脚本都是通过扫描IP端的开放端口并使用常见的密码进行登录尝试,因此修改端口号也是非常有必要的. ...

  2. Google两步验证安装使用方法

    http://www.williamlong.info/archives/2754.html

  3. Google 推出全新的两步验证机制

    近日 Google 在官方的 Apps Updates 博客公布了全新的两步验证功能--Google 提示,新的功能通过与 Google App 联动,进一步将验证确认工作缩减到仅有两步,同时支持 A ...

  4. 两步验证Authy时间同步问题

    Authy是我常用的软件之一,通常用于Google的两步验证,或者是其他基于Google两步验证的原理的衍生程序.比如Namesilo.印象笔记等均有使用. 先说说什么是两步验证. 两步验证 两步验证 ...

  5. 两步验证杀手锏:Java 接入 Google 身份验证器实战

    两步验证 大家应该对两步验证都熟悉吧?如苹果有自带的两步验证策略,防止用户账号密码被盗而锁定手机进行敲诈,这种例子屡见不鲜,所以苹果都建议大家开启两步验证的. Google 的身份验证器一般也是用于登 ...

  6. SecureCRT两步验证自动登录脚本

    简介 用于解决 Google Authenticator 的两步验证登录.涉及到密码,不建议脚本保存到公共环境. 安装oathtool Mac $ brew install oath-toolkit ...

  7. 使用KeePass管理两步验证

    目录 使用KeePass管理两步验证 两步验证 KeePass中管理两步验证 KeeTrayTOTP插件使用 使用KeePass管理两步验证 文:铁乐与猫 2018-9-9 KeePass 是一款管理 ...

  8. OPTAUTH 两步验证详解

    先贴图: 在对外网开放的后台管理系统中,使用静态口令进行身份验证可能会存在如下问题: (1) 为了便于记忆,用户多选择有特征作为密码,所有静态口令相比动态口令而言,容易被猜测和破解: (2) 黑客可以 ...

  9. 七牛云如何绑定二次验证码_虚拟MFA_两步验证_谷歌身份验证器?

    一般情况下,点账户名——账户设置——安全设置,即可开通两步验证 具体步骤见链接  七牛云如何绑定二次验证码_虚拟MFA_两步验证_谷歌身份验证器?   二次验证码小程序(官网)对比谷歌身份验证器APP ...

随机推荐

  1. 在linux查询本机的公网IP

    linux服务器查看公网IP信息的方法 最近在解决网络问题时,需要查看本机的出口公网IP信息,所以在网络上搜索和请求运维达人,获得如下两个方法: curl ifconfig.me 在linux系统中输 ...

  2. 机器学*——K*邻算法(KNN)

    1 前言 Kjin邻法(k-nearest neighbors,KNN)是一种基本的机器学*方法,采用类似"物以类聚,人以群分"的思想.比如,判断一个人的人品,只需观察他来往最密切 ...

  3. 3.8学习总结——Android保存信息

    为了保存软件的设置参数,Android平台为我们提供了一个SharedPreferences接口,它是一个轻量级的存储类,特别适合用于保存软件配置参数.使用SharedPreferences保存数据, ...

  4. jmeter加密解密(加密篇)

    最近刚好在弄jmeter加密解密,可以分享下.(有一段时间没写了,有点不知道从何写起,这篇写的有点乱o(╥﹏╥)o) 需求是:接口中的请求体的部分参数需要先加密再请求,返回的结果中部分字段需解密. 1 ...

  5. css Table 表格宽度失效解决方案

    使用div包裹内容进行支撑 <table cellspacing="0"> <caption>89 HOLLAND ROAD SINGAPORE 27575 ...

  6. 关于go mod 的使用和goland 配置 go mod

    一.关于go modules 1.1 go modules 是go1.11 新加的特性 现在已有go 1.13.4 了本人用了就是最新版的 1.2关于modules 官方定义 模块是相关Go包的集合. ...

  7. Go变量与基础数据类型

    一.基础介绍 Go 是静态(编译型)语言,是区别于解释型语言的弱类型语言(静态:类型固定,强类型:不同类型不允许直接运算) 例如 python 就是动态强类型语言 1.Go 的特性: 跨平台的编译型语 ...

  8. SpringBoot之网站的登陆注册逻辑

    网站的登录注册实现逻辑 该文章主要是为了整理之前学习项目中的知识点,并进行一定程度的理解. 技术列表: SpringBoot MySQL redis JWT 用户登录逻辑: 首先打开前端登录页面,F1 ...

  9. centos8安装MySQL8——通过yum

    centos8上通过yum安装MySQL,过程简单,不易出错 1.检查系统是否已安装MySQL相关,如果有则全部清除干净 #列出MySQL相关的安装包 rpm -qa | grep mysql #依次 ...

  10. Rafy 框架 - 实体支持只更新部分变更的字段

    Rafy 快一两年没有大的更新了.并不是这个框架没人维护了.相反,主要是因为自己的项目.以及公司在使用的项目,都已经比较稳定了,也没有新的功能添加.但是最近因为外面使用了 Rafy 的几个公司,找到我 ...