介绍
选用大二的web大作业,一个知识管理系统——“微知”,为基础来实现双因子用户认证模块,原本的认证方式是匹配用户名和口令,且口令在数据库中是明文存储的。
首先,先删除password字段,口令不再存储在数据库。
其次,对于散列函数,我选择了php中的md5()函数,它的返回值长度为32位。
实现加盐
在用户注册/修改口令(解释见说明)的时候随机生成一个32位的盐值(关于盐值长度的一个经验值是和hash函数的返回值长度保持一致),然后用md5()函数计算“用户输入的口令+盐值”字符串的散列值。将盐值和该散列值分别存入数据库中,字段分别是Salt和PasswordSHash(在原数据库设计的基础上新增的字段)。
认证时,在服务端通过用户输入的用户名,获取数据库中该用户对应的Salt数据,然后把Salt加到用户输入的口令值后面,再用md5()进行计算出散列值,最后将计算出的散列值和数据库中的PasswordSHash进行对比。
谷歌认证器的一次验证码
需要新增字段secretKey,用来保存每个用户(设备)的秘钥。在用户注册时,服务端随机生成一个16位的secretKey,既要在前端反馈给用户,又要存储在数据库中。用户获得到这个secretKey之后,添加到谷歌认证器中,认证器就会每隔30s提供一个6位的一次密码。
认证时,每当用户发送登录请求时,服务端会根据用户输入的用户名和口令先利用盐值和散列函数进行第一步认证。如果通过了认证,那么服务端会从数据库中获取该用户的secretKey,并利用它计算一次密码,和用户输入的一次密码进行比对。如果两方面的认证都通过了,才能成功登录。
核心代码
用户注册接口中的核心代码:生成盐值和秘钥、计算散列值,并存入数据库。
用户登录接口中的核心代码:验证加盐口令的散列值,验证谷歌认证器提供的一次验证码。(上文是先通过用户名查表获得盐值、秘钥和散列值)
截图
-
用手机访问注册页面,填写注册信息,用户名hznucs163,密码为20190101,提交注册之后,服务端将为该用户生成的谷歌认证器的16位秘钥和二维码用json的格式返回。
-
将秘钥文本复制,在谷歌认证器中输入来添加账户,可以看到最后一个账户就是新添加的“网络信息安全”。
- 测试用户认证模块,分别输入错误的密码和错误的一次验证码,(后端逻辑是先验证口令再验证一次验证码的),错误提示分别如下:
-
输入正确的密码和从谷歌认证器提供的一次验证码,登录成功。(一次验证码是339629,在上面的图片上有)
-
查看数据库里该用户的数据(秘钥、盐值和散列值)
-
遇到的问题及解决方案:
生成的secretKey二维码在谷歌认证器中无法识别,提示“不是有效的身份验证令牌条形码”,一开始以为是秘钥字符串格式的原因,它好像需要16位秘钥每4位加一个空格。但把二维码改成这种格式后仍然不成功,于是在注册成功后返回了秘钥字符串,让用户复制后,自行去认证器里添加。
最终解决方案:
把text=后面的内容改成上面那样。本来是直接加$secretkey的。
说明
- 关于盐值的位置。在本次实验中我是直接将盐值放在口令的最后的,实际上Salt不一定加在明文口令的最后,可以插在任意位置,甚至可以把Salt分段插入密码;
- 关于盐值的长度。不能太短,如果太短,攻击者完全可以穷举盐值;它的经验值是至少要和hash函数的返回值长度保持一致;
- 加盐值的好处之一。由于盐值的不同,就算两个用户的口令值完全一样,散列值也不相同,增加了攻破口令的难度,特别是保护了弱密码。
- 每一次修改口令时,都要随机生成一个新的盐值。因为如果口令泄露了,攻击者就可以通过口令和散列值推断出盐值,如此一来,就算用户修改了口令,而盐值不变,攻击者还是能根据旧盐值准备密码表。
参考文章
- 《PHP实现谷歌二次验证实现》
https://www.jianshu.com/p/009c102c3218- 《推荐六个免费在线生成网址二维码的API接口》
https://blog.csdn.net/YAN914/article/details/75747329- 《加密盐的意义和用途》
https://www.cnblogs.com/birdsmaller/p/5377104.html