在传统的系统登录中,通常是使用固定的账号密码登录再配上验证码防止机器人和暴力破解。
这里我将引出两个问题:
1、固定的账号密码在有病毒的电脑或者公共场合下进行登录时,那么在输入账号密码账号密码有可能会被黑客记录登。
2、某些网站不慎泄露了你的固定密码,这时你的密码就会被售卖到各种暗网之中,然后被有心人拿去撞库。
那么双因子认证(2FA)一般会有哪些手段可以用来保护我们的账号安全,我举几个例子说明:
1、短信或语音验证码(SMS)
用户在登录时输入用户名和密码后,会收到一条包含验证码的短信,用户需要输入该验证码才能完成登录。
2、邮件验证码(Email)
用户在登录时输入用户名和密码后,会收到一封包含验证码的电子邮件,用户需要输入该验证码才能完成登录。
3、生物识别(Biometric)
用户通过生物特征如指纹、面部识别、声纹等来完成身份验证。常见的应用场景包括手机指纹识别、面部识别解锁等。
4、图形密码(Pattern-based)
用户需要在一个网格上画出预定的图形,这和生物识别一般都在移动端手机上常用。
5、基于时间的一次性密码(TOTP-Time-based One-Time Password)
用户使用手机上的认证应用(如Google Authenticator(国外)、Microsoft Authenticator(国内))生成一次性密码。这些密码通常每30秒变化一次,用户在登录时输入应用当前生成的密码。上面两种Authenticator都可以使用,当然其他能够实现TOTP的软件同样可用。这里我就介绍两款,一是谷歌的是默认自动同步,二是微软家的需要手动开启同步。自动同步的好处就是当我们不小心把Authenticator软件删除了,是可以通过他们家的云端进行恢复,当然你能本地备份是最好。
现在大多数系统基本上都是基于以上五种方式来进行安全登录,当然还有其他验证方式就不一一列举了。
第一种短信或语音会受到运营商、网络、成本等限制
第二种邮件本身也会受到固定账号密码的安全限制,这里引出来第二个问题就是:不同的平台最好设置不同类型的密码,以防止一次泄露多系统被撞库的问题。
第三种和第四种就是通常我们手机上需要设置,在有必要的情况还是设置图形和密码验证。
第五种也就是今天我们主讲的一种加密手段了,在我们以前玩游戏有听说过“将军令”、“令牌”就是TOTP的一个产物。
首先我们来认识一下什么是OTP:
OTP、HOTP和TOTP都是一次性密码(One-Time Password, OTP)技术的不同实现方式。
他们的公式如下:
OTP(K,C) = Truncate(HMAC-SHA-1(K,C))
K是一串秘钥串,可以使任意字符,我们的秘钥
C是一串数字,表示随机数、动态密码
OTP(One-Time Password)
OTP是一种在每次认证时都生成不同的密码,避免了传统静态密码的安全问题。OTP本身是一个广义的概念,包括了HOTP和TOTP。它确保每个密码只能使用一次,极大地提高了安全性。
HOTP(HMAC-Based One-Time Password)
HOTP是一种基于HMAC(哈希消息认证码)的OTP算法,其定义在RFC 4226中。
- 工作原理:
- HOTP使用一个共享的秘密密钥(secret key)和一个计数器(counter)。
- 生成一次性密码时,HOTP将计数器的值和秘密密钥通过HMAC-SHA-1算法计算哈希值,然后取该哈希值的一部分作为一次性密码。
- 每次生成密码后,计数器递增。
- 特点:
- 事件驱动:密码的生成和验证依赖于计数器的值。
- 同步问题:需要服务器和客户端保持计数器同步,否则可能导致认证失败。
- 应用场景:适用于硬件令牌等场景。
TOTP(Time-Based One-Time Password)
TOTP是一种基于时间的OTP算法,其定义在RFC 6238中。
- 工作原理:
- TOTP使用一个共享的秘密密钥(secret key)和当前时间的一个时间步长(time step)。
- 生成一次性密码时,TOTP将当前时间(通常以30秒为步长)和秘密密钥通过HMAC-SHA-1(或SHA-256/512)算法计算哈希值,然后取该哈希值的一部分作为一次性密码。
- 时间步长通常是30秒,但可以根据需求进行调整。
- 特点:
- 时间驱动:密码的生成和验证依赖于当前时间。
- 无需同步:只需保证客户端和服务器的时间大致同步,不需要事件驱动的计数器。
- 应用场景:适用于软件令牌,如Google Authenticator等。
区别和差异
- 生成机制:
- HOTP:基于事件(计数器)的变化生成一次性密码。
- TOTP:基于时间的变化生成一次性密码。
- 同步方式:
- HOTP:需要服务器和客户端保持计数器同步。
- TOTP:只需确保服务器和客户端的时间大致同步。
- 适用场景:
- HOTP:适用于硬件令牌等需要事件驱动的场景。
- TOTP:适用于软件令牌,广泛应用于移动设备上的认证应用。
- 安全性:
- HOTP:如果计数器不同步,可能导致认证失败。
- TOTP:基于时间步长,具有较高的安全性,只需保证时间同步。
实现TOTP对账户安全加强和系统的安全保障:
首先我们来看看效果效果
绑定验证器
手机扫码获取6位数动态密码
绑定成功
登录系统
登录成功
总之,这为我们的账号密码多了一份安全保障。
安全防护方面我们始终不能掉以轻心,即使多了一份保障,我们也需要养成网络安全的习惯。
例如:定期更换密码、定期给电脑杀毒清理垃圾、更换服务器高危端口、漏洞定期修复、避免弱密码等
在整个互联网中,看似无声无息,其实聒噪不堪。
撞库、破解、渗透等手段在悄无声息的进行着,所以我们需要无时无刻关注安全。
接下来我将以netcore的代码实现TOPT的案例以供参考。
代码实现
创建一个验证码帮助类
GoogleAuthenticatorHelper.class
/// <summary>
/// 谷歌双因子验证帮助类
/// </summary>
public static class GoogleAuthenticatorHelper
{
private readonly static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly static TimeSpan DefaultClockDriftTolerance = TimeSpan.FromSeconds(30);
/// <summary>
/// 生成双因子验证
/// </summary>
/// <param name="issuer">颁发者</param>
/// <param name="user">用户账号</param>
/// <param name="key">生成的key</param>
/// <returns></returns>
public static SetupCode GenerateSetupCode(string issuer, string user, string key)
{
byte[] keyArr = Encoding.UTF8.GetBytes(key);
return GenerateSetupCode(issuer, user, keyArr);
}
/// <summary>
/// 生成双因子验证
/// </summary>
/// <param name="issuer">颁发者</param>
/// <param name="user">用户账号</param>
/// <param name="keyArr">生成的key</param>
/// <returns></returns>
/// <exception cref="NullReferenceException"></exception>
public static SetupCode GenerateSetupCode(string issuer, string user, byte[] keyArr)
{
user = RemoveWhitespace(user);
string encodedSecretKey = Base32Encoding.ToString(keyArr).Replace("=", "");
string provisionUrl = String.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", user, encodedSecretKey, issuer);
SetupCode setupCode = new SetupCode();
setupCode.provisionUrl = provisionUrl;
setupCode.encodedSecretKey = encodedSecretKey;
return setupCode;
}
/// <summary>
/// 清理空白
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private static string RemoveWhitespace(string str)
{
return new string(str.Where(c => !Char.IsWhiteSpace(c)).ToArray());
}
private static string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6)
{
return GenerateHashedCode(accountSecretKey, counter, digits);
}
private static string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
{
byte[] key = Encoding.UTF8.GetBytes(secret);
return GenerateHashedCode(key, iterationNumber, digits);
}
private static string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6)
{
byte[] counter = BitConverter.GetBytes(iterationNumber);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(counter);
}
HMACSHA1 hmac = new HMACSHA1(key);
byte[] hash = hmac.ComputeHash(counter);
int offset = hash[hash.Length - 1] & 0xf;
// Convert the 4 bytes into an integer, ignoring the sign.
int binary =
((hash[offset] & 0x7f) << 24)
| (hash[offset + 1] << 16)
| (hash[offset + 2] << 8)
| (hash[offset + 3]);
int password = binary % (int)Math.Pow(10, digits);
return password.ToString(new string('0', digits));
}
private static long GetCurrentCounter()
{
return GetCurrentCounter(DateTime.UtcNow, _epoch, 30);
}
private static long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep)
{
return (long)(now - epoch).TotalSeconds / timeStep;
}
public static bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient)
{
return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance);
}
public static bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance)
{
var codes = GetCurrentPINs(accountSecretKey, timeTolerance);
return codes.Any(c => c == twoFactorCodeFromClient);
}
private static string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance)
{
List<string> codes = new List<string>();
long iterationCounter = GetCurrentCounter();
int iterationOffset = 0;
if (timeTolerance.TotalSeconds > 30)
{
iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00);
}
long iterationStart = iterationCounter - iterationOffset;
long iterationEnd = iterationCounter + iterationOffset;
for (long counter = iterationStart; counter <= iterationEnd; counter++)
{
codes.Add(GeneratePINAtInterval(accountSecretKey, counter));
}
return codes.ToArray();
}
}
创建一个载荷Dto类
SetupCode.class
/// <summary>
/// 谷歌验证dto
/// </summary>
public class SetupCode
{
/// <summary>
/// 返回的扫码url
/// </summary>
public string provisionUrl;
/// <summary>
/// 返回的手动输入key
/// </summary>
public string encodedSecretKey;
}
生成和验证
//系统生成给用户绑定
var setCode = GoogleAuthenticatorHelper.GenerateSetupCode("颁发者", "用户名", "系统生成的key");
//系统验证用户发送的动态码
var isAuthOk = GoogleAuthenticatorHelper.ValidateTwoFactorPIN("系统生成的key","6位数动态密码");
至此整个核心代码已经完成,具体的存储验证可以根据自己的系统体系进行合并。
我后台的实现方案是创建一个认证表,关联用户id和生成的key,有效期5分钟内完成,在规定时间内绑定后就会对用户表中的两个字段(2fa是否开启字段和2fa认证表的主键id字段)进行更新。
当用户登录时先去查询是否开启双因子认证,如果是就提示先输入6六位数动态密码一同提交验证。
代码解释
代码中有这么一段代码
private readonly static TimeSpan DefaultClockDriftTolerance = TimeSpan.FromSeconds(30);
这句代码的意思是设置时间步数:30秒,在RFC 6238文档中默认也是30秒,间隔时间内就会产生一个6位数动态密码。
在0-29秒时如果如果还没输入在0-29秒生成的6位数动态密码就会验证失败。
在30-59秒时如果如果还没输入在30-59秒生成的6位数动态密码就会验证失败。
这里会产生一个问题
当用户可能在28秒或者58秒的时候输入当时生成的6位数动态密码,由于网络传输延迟、程序反应时间、数据库查询耗时等因素导致程序会验证的是下一次生成动态密码,最终导致用户登录失败的情况。
那么我们可以把这个默认容忍度调高一点,例如:1分钟、5分钟
private readonly static TimeSpan DefaultClockDriftTolerance = TimeSpan.FromSeconds(60或300);
那么一旦设置了容忍度之后就会把此时动态密码的前后产生的动态密码进行容忍度验证。
例如:
30秒时只会产生1个六位数动态密码
60秒(1分钟)会产生5个六位数动态密码
300秒(5分钟)会产生21个六位数动态密码
容忍度示例代码
这里写两个测试方法,一个生成,一个验证
/// <summary>
/// 生成
/// </summary>
/// <returns></returns>
[HttpGet]
public SetupCode create()
{
//系统生成给用户绑定
var setCode = GoogleAuthenticatorHelper.GenerateSetupCode("颁发者", "用户名", "系统生成的key");
return setCode;
}
/// <summary>
/// 验证
/// </summary>
/// <returns></returns>
[HttpGet]
public bool valid(string code, int step)
{
//系统验证用户发送的动态码
return GoogleAuthenticatorHelper.ValidateTwoFactorPIN("系统生成的key", code, TimeSpan.FromSeconds(step));
}
当我们测试30秒时,我们可以看到,只有一个密码可供验证
当我们测试1分钟时,我们可以看到5个密码可供验证
当我们测试5分钟时,我们可以看到21个密码可供验证
还有就是二维码的生成我就没有使用后台生成了,因为系统架构是前后端分离,所以都交给前端啦!
总之,在整个系统建设中,我认为安全是重中之重。
系统一旦被黑客或有心人给突破,那么面临的损失将会不可估量。