vue_drf之实现极验滑动验证码
一、需求
1,场景
我们在很多登录和注册场景里,为了避免某些恶意攻击程序,我们会添加一些验证码,也就是行为验证,让我们相信现在是一个人在交互,而不是一段爬虫程序。现在市面上用的比较多的,比较流行的是极验的滑动验证码。
2,伪代码
1,当打开登录页面时,页面还没加载完毕,浏览器就自动往服务器发送一个get请求,主要是请求极验滑动验证码的相关数据,页面接收到相关数据后,在页面渲染出一个滑动验证码组件,
2,用户输入用户名和密码后,点击滑动验证码,进行验证,验证成功后会自动往服务器发送一个post请求,服务器会产生一个随机数,保存在redis中,然后也把这个随机数返回
3,当验证码验证成功后,用户点击登录按钮,这次会发送post请求,携带用户名,密码,接收到的随机数,服务器对接收到的数据进行校验,当验证成功后,给前端返回一个token,token中包含用户的信息,然后前端再跳转到其他页面上
二、代码
login.vue
<template> <div id="login"> <div class="box"> <p> <img src="../../assets/login_title.png" alt=""> </p> <p class="sign">帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p> <div class="pass" v-show="num==1"> <div class="title2 cursor"> <span @click="num=1" :class="num==1 ? 'show' :''">密码登录</span> <span @click="num=2" :class="num==2 ? 'show' :''">短信登录</span> </div> <input v-model="username" type="text" class="ss" placeholder="用户名 / 手机号码"> <input v-model="password" type="password" class="ss" placeholder="密码"> <div id="captcha" class="ss"></div> <div class="t1"> <div class="left"> <input type="checkbox" class="cursor" v-model="remenber"> <div class="remenber cursor" >记住密码</div> </div> <div class="right cursor">忘记密码</div> </div> <button class="login_btn" @click="login1">登录</button> <div class="register"> 没有账号 <span><router-link to="/register">立即注册</router-link></span> </div> </div> <div class="messge" v-show="num==2"> <div class="title2 cursor"> <span @click="num=1" :class="num==1 ? 'show' :''">密码登录</span> <span @click="num=2" :class="num==2 ? 'show' :''">短信登录</span> </div> <input v-model="phone" type="text" class="ss" placeholder="手机号码"> <div class="sms"> <input v-model="sms_code" type="text" class="ss"> <div class="content" @click="get_sms_code">{{content}}</div> </div> <button class="login_btn" @click="sms_login">登录</button> <div class="register"> 没有账号 <span><router-link to="/register">立即注册</router-link></span> </div> </div> </div> </div> </template> <script> export default { name:'login', data:function () { return { num:1, username:'', password:'', remenber:'', status:'', content:'获取验证码', phone:'', sms_code:'', verify_code:'', } }, methods:{ //手机号和短信验证码登录 sms_login:function(){ let _this=this; this.$axios.post('http://127.0.0.1:8000/user/login/',{ 'username':_this.phone, 'password':_this.sms_code, 'type':'phone', },{responseType:'json'}) .then(function (res) { sessionStorage.token=res.data.token; _this.$router.push('/home'); }).catch(function (error) { console.log(error.response) }); }, //获取短信验证码 get_sms_code:function(){ let reg = /1[3-9]{2}\d{8}/; if( reg.test(this.phone) ){ if(this.content == "获取验证码"){ let _this=this; this.$axios.get('http://127.0.0.1:8000/user/sms?type=login&phone='+this.phone) .then(function (res) { if(res.data.message==0){ _this.content=60; let this_=_this; let tt=setInterval(function () { if (this_.content>=1){ this_.content-- } else { this_.content='获取验证码'; clearInterval(tt) } },1000); alert('验证码发送成功'); } }).catch(function (error) { console.log(error.response) }) } }else { alert('手机号码有误') } }, //用户名和密码登录 login1:function () { if (this.status==1){ let _this=this; this.$axios.post('http://127.0.0.1:8000/user/login/',{ 'username':_this.username, 'password':_this.password, 'verify_code':_this.verify_code, 'type':'username' },{responseType:'json'}) .then(function (res) { if (res.status==200){ if (_this.remenber){ sessionStorage.removeItem('token'); localStorage.token=res.data.token; } else { localStorage.removeItem('token'); sessionStorage.token=res.data.token } _this.$router.push('/home'); } else { alert('用户名或密码错误') } }) .catch(function (error) { console.log(error.response) // alert(error.response.data.non_field_errors[0]); }); } else { alert('验证码错误') } }, //二次校验滑动验证码 handlerPopup:function (captchaObj) { let _this=this; captchaObj.onSuccess(function () { var validate = captchaObj.getValidate(); _this.$axios.post("http://127.0.0.1:8000/user/yzm/",{ geetest_challenge: validate.geetest_challenge, geetest_validate: validate.geetest_validate, geetest_seccode: validate.geetest_seccode, },{ responseType:"json", }).then(function (res) { _this.verify_code=res.data.verify_code; _this.status=res.data.status }).catch(function (error) { console.log(error) }) }); captchaObj.appendTo("#captcha"); } }, //请求滑动验证码 created:function () { let _this=this; this.$axios.get("http://127.0.0.1:8000/user/yzm") .then(function (res) { let data=JSON.parse(res.data); initGeetest({ width:'350px', gt: data.gt, challenge: data.challenge, product: "popup", offline: !data.success }, _this.handlerPopup); }).catch(function (error) { console.log(error) }) } } </script> <style scoped> #login{ background: url('../../assets/Login.jpg'); background-size: 100% 100%; height: 100%; position: fixed; width: 100%; } .box{ width: 500px; height: 600px; margin: 0 auto; margin-top: 200px; text-align: center; } .box img{ width: 190px; height: auto; } .box p{ margin: 0; } .sign{ font-size: 18px; color: #fff; letter-spacing: .29px; padding-top: 10px; padding-bottom: 50px; } .pass{ width: 400px; height: 460px; margin: 0 auto; background-color: white; border-radius: 4px; } .messge{ width: 400px; height: 390px; margin: 0 auto; background-color: white; border-radius: 4px; } .title2{ width: 350px; font-size: 20px; color: #9b9b9b; padding-top: 50px; border-bottom: 1px solid #e6e6e6; margin: 0 auto; margin-bottom: 20px; } .ss{ width: 350px; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; margin-bottom: 20px; } .pass .t1{ width: 350px; margin: 0 auto; height: 20px; line-height: 20px; font-size: 12px; text-align: center; position: relative; } .t1 .right{ position: absolute; right: 0; } .remenber{ display: inline-block; position: absolute; left: 20px; } .left input{ position: absolute; left:0; width: 14px; height: 14px; } .login_btn{ width: 350px; height: 45px; background: #ffc210; border-radius: 5px; font-size: 16px; color: #fff; letter-spacing: .26px; margin-top: 30px; outline: none; border:none; cursor: pointer; } .register{ margin-top: 20px; font-size: 14px; color: #9b9b9b; } .register span{ color: #ffc210; cursor: pointer; } .cursor{ cursor: pointer; } .show{ display: inline-block; padding-bottom: 5px; border-bottom: 2px solid orange; color: #4a4a4a; } a{ text-decoration: none; color: #ffc210; } #captcha{ margin: 0 auto; height: 44px; } .sms{ position: relative; width: 350px; height: 45px; margin: 0 auto; line-height: 45px; } .sms .content{ position: absolute; top:0; right: 10px; color: orange; border-left: 1px solid orange; padding-left: 10px; cursor: pointer; } </style>
这是上面login.vue代码中的逻辑处理部分
export default { name:'login', data:function () { return { num:1, username:'', password:'', remenber:'', status:'', content:'获取验证码', phone:'', sms_code:'', verify_code:'', } }, methods:{//用户名和密码登录 login1:function () { if (this.status==1){ let _this=this; this.$axios.post('http://127.0.0.1:8000/user/login/',{ 'username':_this.username, 'password':_this.password, 'verify_code':_this.verify_code, 'type':'username' },{responseType:'json'}) .then(function (res) { if (res.status==200){ if (_this.remenber){ sessionStorage.removeItem('token'); localStorage.token=res.data.token; } else { localStorage.removeItem('token'); sessionStorage.token=res.data.token } _this.$router.push('/home'); } else { alert('用户名或密码错误') } }) .catch(function (error) { console.log(error.response) // alert(error.response.data.non_field_errors[0]); }); } else { alert('验证码错误') } }, //二次校验滑动验证码 handlerPopup:function (captchaObj) { let _this=this; captchaObj.onSuccess(function () { var validate = captchaObj.getValidate(); _this.$axios.post("http://127.0.0.1:8000/user/yzm/",{ geetest_challenge: validate.geetest_challenge, geetest_validate: validate.geetest_validate, geetest_seccode: validate.geetest_seccode, },{ responseType:"json", }).then(function (res) { _this.verify_code=res.data.verify_code; _this.status=res.data.status }).catch(function (error) { console.log(error) }) }); captchaObj.appendTo("#captcha"); } }, //请求滑动验证码 created:function () { let _this=this; this.$axios.get("http://127.0.0.1:8000/user/yzm") .then(function (res) { let data=JSON.parse(res.data); initGeetest({ width:'350px', gt: data.gt, challenge: data.challenge, product: "popup", offline: !data.success }, _this.handlerPopup); }).catch(function (error) { console.log(error) }) } }
geetest.py 后端滑动验证码的依赖文件
#!coding:utf8 import sys import random import json import requests import time from hashlib import md5 if sys.version_info >= (3,): xrange = range VERSION = "3.0.0" class GeetestLib(object): """极验验证码的核心类""" FN_CHALLENGE = "geetest_challenge" FN_VALIDATE = "geetest_validate" FN_SECCODE = "geetest_seccode" GT_STATUS_SESSION_KEY = "gt_server_status" API_URL = "http://api.geetest.com" REGISTER_HANDLER = "/register.php" VALIDATE_HANDLER = "/validate.php" JSON_FORMAT = False def __init__(self, captcha_id, private_key): self.private_key = private_key self.captcha_id = captcha_id self.sdk_version = VERSION self._response_str = "" def pre_process(self, user_id=None,new_captcha=1,JSON_FORMAT=1,client_type="web",ip_address=""): """ 验证初始化预处理. //TO DO arrage the parameter """ status, challenge = self._register(user_id,new_captcha,JSON_FORMAT,client_type,ip_address) self._response_str = self._make_response_format(status, challenge,new_captcha) return status def _register(self, user_id=None,new_captcha=1,JSON_FORMAT=1,client_type="web",ip_address=""): pri_responce = self._register_challenge(user_id,new_captcha,JSON_FORMAT,client_type,ip_address) if pri_responce: if JSON_FORMAT == 1: response_dic = json.loads(pri_responce) challenge = response_dic["challenge"] else: challenge = pri_responce else: challenge=" " if len(challenge) == 32: challenge = self._md5_encode("".join([challenge, self.private_key])) return 1,challenge else: return 0, self._make_fail_challenge() def get_response_str(self): return self._response_str def _make_fail_challenge(self): rnd1 = random.randint(0, 99) rnd2 = random.randint(0, 99) md5_str1 = self._md5_encode(str(rnd1)) md5_str2 = self._md5_encode(str(rnd2)) challenge = md5_str1 + md5_str2[0:2] return challenge def _make_response_format(self, success=1, challenge=None,new_captcha=1): if not challenge: challenge = self._make_fail_challenge() if new_captcha: string_format = json.dumps( {'success': success, 'gt':self.captcha_id, 'challenge': challenge,"new_captcha":True}) else: string_format = json.dumps( {'success': success, 'gt':self.captcha_id, 'challenge': challenge,"new_captcha":False}) return string_format def _register_challenge(self, user_id=None,new_captcha=1,JSON_FORMAT=1,client_type="web",ip_address=""): if user_id: register_url = "{api_url}{handler}?gt={captcha_ID}&user_id={user_id}&json_format={JSON_FORMAT}&client_type={client_type}&ip_address={ip_address}".format( api_url=self.API_URL, handler=self.REGISTER_HANDLER, captcha_ID=self.captcha_id, user_id=user_id,new_captcha=new_captcha,JSON_FORMAT=JSON_FORMAT,client_type=client_type,ip_address=ip_address) else: register_url = "{api_url}{handler}?gt={captcha_ID}&json_format={JSON_FORMAT}&client_type={client_type}&ip_address={ip_address}".format( api_url=self.API_URL, handler=self.REGISTER_HANDLER, captcha_ID=self.captcha_id,new_captcha=new_captcha,JSON_FORMAT=JSON_FORMAT,client_type=client_type,ip_address=ip_address) try: response = requests.get(register_url, timeout=2) if response.status_code == requests.codes.ok: res_string = response.text else: res_string = "" except: res_string = "" return res_string def success_validate(self, challenge, validate, seccode, user_id=None,gt=None,data='',userinfo='',JSON_FORMAT=1): """ 正常模式的二次验证方式.向geetest server 请求验证结果. """ if not self._check_para(challenge, validate, seccode): return 0 if not self._check_result(challenge, validate): return 0 validate_url = "{api_url}{handler}".format( api_url=self.API_URL, handler=self.VALIDATE_HANDLER) query = { "seccode": seccode, "sdk": ''.join( ["python_",self.sdk_version]), "user_id": user_id, "data":data, "timestamp":time.time(), "challenge":challenge, "userinfo":userinfo, "captchaid":gt, "json_format":JSON_FORMAT } backinfo = self._post_values(validate_url, query) if JSON_FORMAT == 1: backinfo = json.loads(backinfo) backinfo = backinfo["seccode"] if backinfo == self._md5_encode(seccode): return 1 else: return 0 def _post_values(self, apiserver, data): response = requests.post(apiserver, data) return response.text def _check_result(self, origin, validate): encodeStr = self._md5_encode(self.private_key + "geetest" + origin) if validate == encodeStr: return True else: return False def failback_validate(self, challenge, validate, seccode): """ failback模式的二次验证方式.在本地对轨迹进行简单的判断返回验证结果. """ if not self._check_para(challenge, validate, seccode): return 0 validate_result = self._failback_check_result( challenge, validate,) return validate_result def _failback_check_result(self,challenge,validate): encodeStr = self._md5_encode(challenge) if validate == encodeStr: return True else: return False def _check_para(self, challenge, validate, seccode): return (bool(challenge.strip()) and bool(validate.strip()) and bool(seccode.strip())) def _md5_encode(self, values): if type(values) == str: values = values.encode() m = md5(values) return m.hexdigest()
views.py #处理滑动验证码
#滑动验证码 class VerifyCode(APIView): status='' user_id='' """验证码类""" def get(self,request): """获取验证码""" user_id = random.randint(1, 100) APP_ID = "xxxxxxxxxxx" #这两个数据都是在极验官网你的个人界面有的 APP_KEY = "xxxxxxxxxxx" gt = GeetestLib(APP_ID,APP_KEY) status = gt.pre_process(user_id,JSON_FORMAT=0, ip_address="127.0.0.1") VerifyCode.status=status # request.session[gt.GT_STATUS_SESSION_KEY] = status # request.session["user_id"] = user_id VerifyCode.user_id=user_id data = gt.get_response_str() return Response(data) def post(self,request): """校验验证码""" APP_ID = "xxxxxxxxxxxxx" APP_KEY = "xxxxxxxxx" gt = GeetestLib(APP_ID, APP_KEY) # status = request.session[gt.GT_STATUS_SESSION_KEY] status=VerifyCode.status challenge = request.data.get(gt.FN_CHALLENGE,'') validate = request.data.get(gt.FN_VALIDATE,'') seccode = request.data.get(gt.FN_SECCODE,'') # user_id = request.session["user_id"] user_id=VerifyCode.user_id if status: result = gt.success_validate(challenge, validate, seccode, user_id) else: result = gt.failback_validate(challenge, validate, seccode) VerifyCode.status='' VerifyCode.user_id='' if result: verify_code = "%06d" % random.randint(0, 99999999) redis = get_redis_connection("verify_code") redis.setex(verify_code,60, 1) result={"status":1,'verify_code':verify_code} else: result = {"status": 0} return Response(result)
utils.py 登录验证用户信息
from django.contrib.auth.backends import ModelBackend from django_redis import get_redis_connection def jwt_response_payload_handler(token, user=None, request=None): """ 自定义jwt认证成功返回数据 :token 返回的jwt :user 当前登录的用户信息[对象] :request 当前本次客户端提交过来的数据 """ return { 'token': token, 'id': user.id, 'username': user.username, } #实现多功能登录 import re from .models import User def get_user_by_account(account): """ 根据帐号获取user对象 :param account: 账号,可以是用户名,也可以是手机号 :return: User对象 或者 None """ try: if re.match('^1[3-9]\d{9}$', account): # 帐号为手机号 user = User.objects.get(phone=account) else: # 帐号为用户名 user = User.objects.get(username=account) except User.DoesNotExist: return None else: return user def sms_code_verify(phone,sms_code): redis = get_redis_connection("sms_code") value=redis.get('sms_%s'%phone).decode() if value==sms_code: return True return False #这是对滑动验证码的随机数进行校验 def verify_code(req): verify_code = req.data.get('verify_code') redis = get_redis_connection('verify_code') value = redis.get(verify_code) return None or value class UsernameMobileAuthBackend(ModelBackend): """ 自定义用户名或手机号认证 """ def authenticate(self, request, username=None, password=None, **kwargs): ty=request.data.get('type') user = get_user_by_account(username) if ty=='phone' and user is not None and sms_code_verify(username,password): return user elif user is not None and user.check_password(password) and verify_code(request): return user else: return None