一、需求
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