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方法的思路:

  1. 根据username参数查找用户User对象,username参数可能是用户名,也可能是手机号

  2. 若查找到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',
]

  

 

在登陆功能中集成极验验证码

  需要先去极验创建账号,并且创建一个新的验证,创建好了以后,就可以得到APPID和APPKEY。

 

 

有了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的代码:
 
<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>
View Code

 

 注意:因为使用极验,需要用到session,但在vue中axios发送ajaxi默认是不携带cookie值,假如需要携带值

需要修改axios的参数 (神坑):
 
在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)
View Code

 

前端代码,需要在接受验证返回值,多接受一个参数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)
View Code

 

后端注册用户视图调用序列化器,需要验证当前验证码.(添加验证字段,验证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
View Cod

posted @ 2019-03-26 22:11  Mixtea  阅读(294)  评论(0编辑  收藏  举报