luffy之多条件登录与极验滑动验证码
多条件登录
JWT扩展的登录视图,在收到用户名与密码时,也是调用Django的认证系统中提供的authenticate()来检查用户名与密码是否正确。
我们可以通过修改Django认证系统的认证后端(主要是authenticate方法)来支持登录账号既可以是用户名也可以是手机号。
修改Django认证系统的认证后端需要继承django.contrib.auth.backends.ModelBackend,并重写authenticate方法。
authenticate(self, request, username=None, password=None, **kwargs)
方法的参数说明:
-
request 本次认证的请求对象
-
username 本次认证提供的用户账号
-
password 本次认证提供的密码
我们想要让用户既可以以用户名登录,也可以以手机号登录,那么对于authenticate方法而言,username参数即表示用户名或者手机号。
重写authenticate方法的思路:
-
根据username参数查找用户User对象,username参数可能是用户名,也可能是手机号
-
若查找到User对象,调用User对象的check_password方法检查密码是否正确
在users/utils.py中编写:
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(mobile=account) else: # 帐号为用户名 user = User.objects.get(username=account) except User.DoesNotExist: return None else: return user class UsernameMobileAuthBackend(ModelBackend): """ 自定义用户名或手机号认证 """ def authenticate(self, request, username=None, password=None, **kwargs): user = get_user_by_account(username) if user is not None and user.check_password(password): return user
在配置文件settings.py中告知Django使用我们自定义的认证后端
AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', ]
有了id和key以后,就可以根据官方的文档进行集成了.
官方: https://docs.geetest.com/install/overview/start/
python集成文档: https://docs.geetest.com/install/deploy/server/python/
SDK: 开发集成工具包
安装依赖模块:
pip install requests
安装完成模块以后,可以参考官方文档中的案例,把提供验证码和校验验证码的功能集成到视图类中.
文档: https://github.com/GeeTeam/gt3-python-sdk
在使用前要将相应的文件下载到项目中:geetest.py
以下是后端验证的代码:users/user/view
from django.shortcuts import render import json,random # Create your views here. from rest_framework.response import Response from rest_framework.views import APIView from luffy.libs.geetest import GeetestLib pc_geetest_id = 'af88c60fe04b********8b626b478c497' pc_geetest_key = '2d8570960*********dd219ca1c6ffe7' class VerifyCodeView(APIView): def get(self,request): user_id = 'test' gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = gt.pre_process(user_id,) request.session[gt.GT_STATUS_SESSION_KEY] = status request.session["user_id"] = user_id response_str = gt.get_response_str() return Response(response_str) def post(self,request):#TODO这里的极验验证在post请求获取session值的时候获取不到 gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = request.session[gt.GT_STATUS_SESSION_KEY] 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"] if status: result = gt.success_validate(challenge, validate, seccode, user_id) else: result = gt.failback_validate(challenge, validate, seccode) result = {"status": "success", } if result else {"status": "fail"} return Response(result)
<script> export default { name: "Login", data() { return { login_type: 1, username: "", password: "", remember: false, } }, methods: { login_submit() { this.$axios.post('http://127.0.0.1:8000/users/login', { 'username': this.username, 'password': this.password }, { responseType: "json" }).then(response => { // 请求成功,保存登陆状态 if (this.remember) { // 记住密码 sessionStorage.removeItem("token"); let data = response.data; localStorage.token = data.token; } else { // 不记住密码 localStorage.removeItem("token"); let data = response.data; sessionStorage.token = data.token; } // 登录成功以后,跳转页面 // this.$router.go(-1); this.$router.push("/home"); }).catch(err => { console.log(err); }) }, handlerEmbed(captchaObj) { // 成功的回调 let _this = this; captchaObj.onSuccess(function () { var validate = captchaObj.getValidate(); _this.$axios.post("http://127.0.0.1:8000/users/verify", { geetest_challenge: validate.geetest_challenge, geetest_validate: validate.geetest_validate, geetest_seccode: validate.geetest_seccode }, { responseType: "json", },).then(response => { // 请求成功 console.log('response',response); console.log('response.data',response.data); // 获取验证结果 }).catch(error => { // 请求失败 console.log(error) }) }); captchaObj.appendTo("#embed-captcha");//将滑动验证图片加载到该地中的元素内 } }, created() { this.$axios.get('http://127.0.0.1:8000/users/verify',).then(resopnse => { var data = JSON.parse(resopnse.data); initGeetest({ gt: data.gt, challenge: data.challenge, product: "embed", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效 offline: !data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注 // 更多配置参数请参见:http://www.geetest.com/install/sections/idx-client-sdk.html#config }, this.handlerEmbed) }).catch(err => { console.log(err) }) } } </script>
注意:因为使用极验,需要用到session,但在vue中axios发送ajaxi默认是不携带cookie值,假如需要携带值
在main.js中修改 import axios from 'axios' axios.defaults.withCredentials=true;//让ajax携带cookie Vue.prototype.$axios = axios;
补充:双向验证
从上面的例子看.当有人用程序不经过前端注册时,只需要在请求体中添加status=success即可跳过短信验证,向后端发送数据,这样不太安全.
所以在后端的值短信验证成功,在后端添加一个随机数,返回给前端,我们将此数值存入到redis中,当后端发送注册请求时后端从redis拿出对应数值即可.
客户端验证: 使用js进行识别判断和校验,但是这里的验证不能保存百分百的安全,
后端验证: 入库前最后判断, 最后一道防火墙.
settings.py中设置一个新的redis数据库保存后端生成的随机码:
CACHES = { ... "slicode_randint":{ "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/3", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } }
views.py
class VerifyCodeView(APIView): def get(self,request): user_id = 'test' gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = gt.pre_process(user_id,) request.session[gt.GT_STATUS_SESSION_KEY] = status request.session["user_id"] = user_id response_str = gt.get_response_str() return Response(response_str) def post(self,request):#TODO这里的极验验证在post请求获取session值的时候获取不到 gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = request.session[gt.GT_STATUS_SESSION_KEY] 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"] if status: result = gt.success_validate(challenge, validate, seccode, user_id) else: result = gt.failback_validate(challenge, validate, seccode) # if result == "success": # # 验证通过了,生成一个随机值保存到redis中,在用户再次提交注册信息的时候,验证是否附带了唯一值 # slicode_randint = random.randint(1000, 9999) slicode_randint = "%08d" %random.randint(0, 99999999) print('slicode_randint',slicode_randint) # # 保存到redis中 redis = get_redis_connection("slicode_randint") redis.setex(slicode_randint, 60 * 10, 1) result = {"status": "success", "randint_code": slicode_randint} if result else {"status": "fail", "randint_code": -1} return Response(result)
前端代码,需要在接受验证返回值,多接受一个参数slicode_randint,并且在data先定义这个值
class VerifyCodeView(APIView): def get(self,request): user_id = 'test' gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = gt.pre_process(user_id,) request.session[gt.GT_STATUS_SESSION_KEY] = status request.session["user_id"] = user_id response_str = gt.get_response_str() return Response(response_str) def post(self,request):#TODO这里的极验验证在post请求获取session值的时候获取不到 gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = request.session[gt.GT_STATUS_SESSION_KEY] 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"] if status: result = gt.success_validate(challenge, validate, seccode, user_id) else: result = gt.failback_validate(challenge, validate, seccode) # if result == "success": # # 验证通过了,生成一个随机值保存到redis中,在用户再次提交注册信息的时候,验证是否附带了唯一值 # slicode_randint = random.randint(1000, 9999) slicode_randint = "%08d" %random.randint(0, 99999999) print('slicode_randint',slicode_randint) # # 保存到redis中 redis = get_redis_connection("slicode_randint") redis.setex(slicode_randint, 60 * 10, 1) result = {"status": "success", "randint_code": slicode_randint} if result else {"status": "fail", "randint_code": -1} return Response(result)
后端注册用户视图调用序列化器,需要验证当前验证码.(添加验证字段,验证slicode_randint与redis中存储的是否一致,验证一致,在存入数据库前需要删除该字段,不能存到数据库中)
# from rest_framework import serializers # from .models import User # import re # from django_redis import get_redis_connection # # class UserModelSerializer(serializers.ModelSerializer): # """用户信息序列化器""" # sms_code = serializers.CharField(label='手机验证码', required=True, allow_null=False, allow_blank=False, write_only=True) # password2 = serializers.CharField(label='确认密码', required=True, allow_null=False, allow_blank=False, write_only=True) # token = serializers.CharField(label='jwt', read_only=True) randint_code= serializers.CharField(label="极验验证验的验证字符串",write_only=True) # class Meta: # model=User fields = ('id','sms_code', 'mobile', 'password','password2','token',"verify_code") # extra_kwargs={ # "password":{ # "write_only":True # }, # "id":{ # "read_only":True, # } # } # def validate_randint_code(self,value): """验证当前客户端是否已经通过了极验验证码的验证""" redis = get_redis_connection("verify_code") verify_code = redis.get(value) print(verify_code) if(not verify_code): raise serializers.ValidationError('验证码失效~') return value # def validate_mobile(self, value): # """验证手机号""" # if not re.match(r'^1[345789]\d{9}$', value): # raise serializers.ValidationError('手机号格式错误') # # # 验证手机号是否已经被注册了 # # try: # # user = User.objects.get(mobile=value) # # except: # # user = None # # # # if user: # # raise serializers.ValidationError('当前手机号已经被注册') # # # 上面验证手机号是否存在的代码[优化版] # try: # User.objects.get(mobile=value) # # 如果有获取到用户信息,则下面的代码不会被执行,如果没有获取到用户信息,则表示手机号没有注册过,可以直接pass # raise serializers.ValidationError('当前手机号已经被注册') # except: # pass # # return value # # def validate(self,data): # """验证密码""" # password = data.get("password") # password2 = data.get("password2") # if len(password)<6: # raise serializers.ValidationError('密码太短不安全~') # # if password !=password2: # raise serializers.ValidationError('密码和确认必须一致~') # # """验证短信验证码""" # # mobile = data.get("mobile") # sms_code = data.get("sms_code") # # # 从redis中提取短信验证码 # redis = get_redis_connection("sms_code") # # 注意,在redis中保存数据的格式,最终都是bytes类型的字符串,所以提取数据时,要转下编码 # redis_sms_code = redis.get("sms_%s" % mobile).decode() # # 把redis中的短信验证码和客户端提交的验证码进行匹配 # if not (redis_sms_code and redis_sms_code == sms_code): # raise serializers.ValidationError('手机验证码无效~') # # return data # # def create(self, validated_data): # # 删除一些不需要保存到数据库里面的字段 # del validated_data['password2'] # del validated_data['sms_code'] del validated_data['randint_code'] # # # 可以补充删除redis的验证码逻辑 # # # 因为数据库中默认用户名是唯一的,所以我们把用户手机号码作为用户名 # validated_data["username"] = validated_data["mobile"] # # # 继续调用ModelSerializer内置的添加数据功能 # user = super().create(validated_data) # # # 针对密码要加密 # user.set_password(user.password) # # 修改密码等用于更新了密码,所以需要保存 # user.save() # # # 一旦注册成功以后,则默认表示当前用户已经登录了 # # 所以后端要生成一个jwt提供给客户端 # from rest_framework_jwt.settings import api_settings # jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER # jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER # # payload = jwt_payload_handler(user) # # 把jwt生成的token作为user模型的字段 # user.token = jwt_encode_handler(payload) # # return users