Google令牌
#0 github
代码语言:javascript复制https://github.com/Coxhuang/google-authenticator.git
#1 使用操作
- 调用绑定google-authenticator的接口,生成一个二维码(如何生成先不用管,后面再说)
- 手机客户端扫描二维码,App生成一个动态的6位验证码
- 输入验证码,返回True/False
#2 原理
Google令牌分成两部分,一部分是服务端(Google提供的开源代码),另一部分就是客户端(用户在手机/电脑上安装的app或者插件)
- (服务端)随机生成一个字符串,并将该字符串 用户唯一标示(这里我用的用户唯一标示是邮箱)构造成固定的格式生成一个二维码
- (客户端)手机下载google-authenticator客户端,扫描二维码,二维码的信息(字符串 用户唯一标示)会保存在客户端内,App通过算法生成一个6位的验证码(验证码会通过时间的变化,30秒更新一次)
- (服务端)服务端使用Google提供的代码,把App提供的验证码 邮箱进行校验
#3 实例讲解
需求分析
- 用户登陆时,除了需要用户名和密码,还需要提供该用户对应的Google令牌验证码
使用步骤
新增用户(跳过这一步骤)
绑定google-authenticator
调用绑定令牌接口效果图
拿返回的数据生成二维码,因为是前后端分离的项目,所以生成二维码交给前端处理,我这里使用网上在线生成二维码工具(https://cli.im/text?3dd4ec76bb965fb69effefc6a95b8ff8)
使用手机App扫描二维码
登陆
输入错误的令牌
输入正确的令牌,会生成token,也就是登陆成功
#4 具体代码讲解(本例子是前后端分离项目,只考虑后端,前端代码忽略,后端代码基于Django RestFramework)
#4.1 需求分析
- 在用户登陆时,除了需要用户提供账号密码,还需要用户提供该用户实时的令牌验证码
- 登陆成功返回TOKEN
#4.2 绑定令牌
先上代码
代码语言:javascript复制class googleSerializer(DynamicFieldsMixin,serializers.ModelSerializer):
username = serializers.CharField()
password = serializers.CharField()
class Meta:
model = models.UserProfile
fields = ["username","password",]
def validate_username(self, username):
user = authenticate(username=username, password=self.initial_data["password"]) # 验证需要绑定令牌的用户账号密码是否[匹配
if not user:
raise Http404("账号密码不匹配")
return username
class googleBindAPI(APIView):
def post(self,request):
queryset = models.Google2Auth.objects.filter(Q(user__username=request.data["username"]) | Q(user__email=request.data["username"]))
if queryset.exists():
return Response({"success": False, "msg": "已经绑定令牌,绑定失败", "results": None},status=status.HTTP_400_BAD_REQUEST)
serializer = googleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = models.UserProfile.objects.get(Q(email=request.data["username"]) | Q(username=request.data["username"]))
login(request, user)
base_32_secret = base64.b32encode(codecs.decode(codecs.encode('{0:020x}'.format(random.getrandbits(80))), 'hex_codec'))
totp_obj = googletotp.TOTP(base_32_secret.decode("utf-8"))
qr_code = re.sub(r'= $', '', totp_obj.provisioning_uri(request.user.email))
models.Google2Auth.objects.create(user=user)
key = str(base_32_secret,encoding="utf-8")
queryset.update(key=key)
return Response({"success": True, "msg": "绑定成功","results": {"将此数据生成二维码": qr_code}}, status=status.HTTP_201_CREATED)
请求头数据(用户名 密码)
代码语言:javascript复制{
"username":"user",
"password":"cox123456"
}
表结构
代码语言:javascript复制class Google2Auth(models.Model):
user = models.OneToOneField(UserProfile,on_delete=models.CASCADE)
key = models.CharField(verbose_name="Google秘钥",max_length=128)
#4.2 登陆
代码语言:javascript复制class loginView(APIView):
def post(self,request):
user = authenticate(username=request.data["username"], password=request.data["password"])
if not user:
raise Http404("账号密码不匹配")
try:
# 判断用户是否已经绑定Google令牌
key = models.Google2Auth.objects.get(
Q(user__username=request.data["username"])|Q(user__email=request.data["username"])).key
except:
raise Http404("未绑定令牌")
if not Google_Verify_Result(key, request.data["code"]):
# 验证令牌
return Response({"success": True, "msg": "令牌失效", "results": None}, status=status.HTTP_400_BAD_REQUEST)
login(request, user)
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return Response({ "success": True, "msg": "登录成功","results": token},status=status.HTTP_200_OK)
总结
随机生成的字符串在客户端保存是通过二维码保存,在服务端保存在数据库中,用户在App上拿到的验证码是App中的算法经过随机字符串 时间戳 其他 生成的(这里的随机字符串和时间戳可以理解为盐),然后用户在登录时,经过服务端的算法时,把用户对应的字符串 验证码 本地时间戳,Google提供的算法会返回是否匹配
App
- Google令牌 扫码器(如果手机只安装Google令牌App扫码失败,请安装扫码器) 链接:https://pan.baidu.com/s/1XeO7p4IvNuvzQOiZrq4wtw 提取码:e70f
- Chrome插件(不需要手机App,用插件就能绑定)
https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai