Django的MFA验证码接口

背景

增加双因子认证,提高网站登录的安全性。利用谷歌身份验证器绑定密钥,从而进行动态MFA验证。

原理

核心内容

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

  • 一个共享密钥
  • 当前时间输入
  • 一个签名函数
加密原理和步骤
  • Step1:base32 secret

    • Key:共享密钥,在Google Authenticator中是通过将一段字符串进行base32解码成bytes得到的。但由于此密钥在不够32位或超过32位时会用’=’表示,故用的pyotp随机生成的密钥。
      1
      2
      Secret = pyotp.random_base32()
      Key = base64.b32decode(Secret, True)
  • Step2:get current timestamp

    • Count:计数器,通过当前时间戳除以30然后将得到的整数转换成一个大端序的字节。
      1
      2
      3
      # int(time.time()) // 30  到当前经历了多少个30秒
      # 将间隔时间转为big-endian(大端序)并且为长整型的字节
      Count = struct.pack(">Q", int(time.time()) // 30)
  • Step3:start hmac-sha1

    • Hmac:将K和C做HMAC-SHA-1加密然后以字节方式保存,因为后期需要进行与运算,而str是不能和int进行与运算的。
      1
      2
      3
      4
      5
      # hmac = SHA1(secret + SHA1(secret + input))
      # 为了方便演示,将字节转换成了字符串显示
      Hmac = hmac.new(K, C, hashlib.sha1).digest()
      # 取出最后一位和数字15做与运算
      O = H[19] & 15
  • Step4:get DynamicPasswd

    • 通过计算出来的O在H中取出4个16进制的字节,然后将字节转换成正整数,因转换后的正整数是放在数组里面的,所以需要使用[0]取出。最后与一个全为1的二进制与运算然后与10^6做取余运算,最终会得到一个6位数的TOTP
      1
      2
      3
      4
      5
      DynamicPasswd = str((struct.unpack(">I", H[O:O + 4])[0] & 0x7fffffff) % 1000000)    
      # struct.unpack('>I',h[o:o+4])[0] :转为big-endian(大端序)并且不为负数的数字(整数),因为转换完是一个数组,类似"(2828101188,)",所以需要[0]取出
      # h[o:o+4] :取其中4个字节 o=10 则取索引分别为 10,11,12,13的字节
      # & 0x7fffffff = 11111111 :与字节转换的数字做与运算
      # % 1000000 :得出的数字与1000000相除然后取余
  • Step5:get MFA

    • 最后计算出的6位数字最左边的一位可能为0,所以需要判断如果DynamicPasswd得到的是一个5位数的数字,那就在最左边加上一个0。
      1
      TOTP = str(0) + str(DynamicPasswd) if len(DynamicPasswd) < 6 else DynamicPasswd

实现步骤

  • 1.使用python的pyotp模块生成谷歌认证需要的密钥
  • 2.根据密钥生成二维码图片以及计算出6位动态验证码
  • 3.使用谷歌的身份验证器app,扫描二维码或者手动输入密钥
  • 4.平台二次认证通过对输入的动态验证码进行校验

相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import base64, time, struct, hmac, hashlib
import pyotp
from qrcode import QRCode
from qrcode import constants

name = 'user01'+':SmartMS'

# 利用参数secretKey,计算Google Authenticator 6位动态码。
def getMFACode(Secret):
print('MFA密钥:{}'.format(Secret))
K = base64.b32decode(Secret, True)
C = struct.pack(">Q", int(time.time()) // 30)
H = hmac.new(K, C, hashlib.sha1).digest()
O = H[19] & 15 # bin(15)=00001111=0b1111
DynamicPasswd = str((struct.unpack(">I", H[O:O + 4])[0] & 0x7fffffff) % 1000000)
TOTP = str(0) + str(DynamicPasswd) if len(DynamicPasswd) < 6 else DynamicPasswd
print('动态MFA:{}'.format(TOTP))
return TOTP

def getMFAImg(name,Secret):
# otpauth://totp/ 固定格式
# name:标识符信息,issuer:发行信息
url = "otpauth://totp/" + name + "?secret=%s" % Secret + "&issuer=Anchnet"
qr = QRCode(version=1, error_correction=constants.ERROR_CORRECT_L,box_size=6,border=4)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image()
# img.show()

def checkCode(Secret):
code = int(input('输入验证码:'))
t = pyotp.TOTP(Secret)
result = t.verify(code)
msg = result if result is True else False
print('验证码验证{}'.format(msg))
return msg

if __name__ == '__main__':
Secret = pyotp.random_base32()
# Secret = 'UFB6R5QKLPV7FGIU'
getMFACode(Secret)
Secret = getMFAImg(name,Secret)
# checkCode(Secret)

封装为接口

  • resful.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 新建一个包,包下创建resful.py文件
from django.http import JsonResponse
class HttpCode(object):
ok = 200
paramserror = 400
unauth = 401
methoderror = 405
servererror = 500

def result(code=HttpCode.ok,message="",data=None,kwargs=None):
json_dict = {"code":code,"message":message,"data":data}

if kwargs and isinstance(kwargs,dict) and kwargs.keys():
json_dict.update(kwargs)

return JsonResponse(json_dict)

def ok():
return result()

def params_error(message="",data=None):
return result(code=HttpCode.paramserror,message=message,data=data)

def unauth(message="",data=None):
return result(code=HttpCode.unauth,message=message,data=data)

def method_error(message="",data=None):
return result(code=HttpCode.methoderror,message=message,data=data)

def server_error(message="",data=None):
return result(code=HttpCode.servererror,message=message,data=data)
  • views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from utils import restful  # 自定义的restful
import base64, time, struct, hmac, hashlib
import pyotp
from qrcode import QRCode
from qrcode import constants

def getMFAinfo(request):
name = request.GET['name'] + ':SmartMS'
# Secret = request.GET['secret']
Secret = pyotp.random_base32()
K = base64.b32decode(Secret, True)
C = struct.pack(">Q", int(time.time()) // 30)
H = hmac.new(K, C, hashlib.sha1).digest()
O = H[19] & 15 # bin(15)=00001111=0b1111
DynamicPasswd = str((struct.unpack(">I", H[O:O + 4])[0] & 0x7fffffff) % 1000000)
TOTP = str(0) + str(DynamicPasswd) if len(DynamicPasswd) < 6 else DynamicPasswd
url = "otpauth://totp/" + name + "?secret=%s" % Secret + "&issuer=Anchnet"
qr = QRCode(version=1, error_correction=constants.ERROR_CORRECT_L, box_size=6, border=4)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image()
codeinfo = {"name":name, "MFAcode":TOTP, "Secret":Secret, "QRurl":url}

return restful.result(message="获取成功",data=codeinfo)
  • 主urls.py
1
2
3
4
5
6
7
8
from django.urls import path,include
from apps.news import views

urlpatterns = [
...
path('account/',include("apps.xfzauth.urls")),
...
]
  • apps的urls.py
1
2
3
4
5
6
7
8
9
10
from django.urls import path
from . import views

app_name = 'xfzauth'

urlpatterns = [
...
path('code/',views.getMFAinfo,name='code'),
...
]

测试

  • 验证码生成测试
    mark

  • 接口测试
    mark

    mark

-------------本文结束感谢您的阅读-------------
原创技术分享,感谢您的支持。