• 参考网址
- https://www.cnblogs.com/lbzbky/articles/11852230.html
  • 后端部署(准备3个文件)
- geetest.py(照抄) # 官方提供的SDK
- captcha_verify(照抄).py # 利用SDK生成的校验函数,发post请求的时候调用,校验验证码是否通过
- captcha.py # 前端页面验证码组件初始化的时候,发送get请求到后端的响应View
	- 返回json,前端存储起来,等发post登录请求的时候需用到
### utils/geetest.py
# !/usr/bin/env python
# -*- coding:utf-8 -*-

import sys
import random
import json
import requests
import time
from hashlib import md5

if sys.version_info >= (3,):
    xrange = range

VERSION = "3.2.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"

    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):
        """
        验证初始化预处理.
        """
        status, challenge = self._register(user_id)
        self._response_str = self._make_response_format(status, challenge)
        return status

    def _register(self, user_id=None):
        challenge = self._register_challenge(user_id)
        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):
        if not challenge:
            challenge = self._make_fail_challenge()
        string_format = json.dumps(
            {'success': success, 'gt': self.captcha_id, 'challenge': challenge})
        return string_format

    def _register_challenge(self, user_id=None):
        if user_id:
            register_url = "{api_url}{handler}?gt={captcha_ID}&user_id={user_id}".format(
                api_url=self.API_URL, handler=self.REGISTER_HANDLER, captcha_ID=self.captcha_id, user_id=user_id)
        else:
            register_url = "{api_url}{handler}?gt={captcha_ID}".format(
                api_url=self.API_URL, handler=self.REGISTER_HANDLER, captcha_ID=self.captcha_id)
        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=''):
        """
        正常模式的二次验证方式.向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
        }
        backinfo = self._post_values(validate_url, query)
        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_str = validate.split('_')
        encode_ans = validate_str[0]
        encode_fbii = validate_str[1]
        encode_igi = validate_str[2]
        decode_ans = self._decode_response(challenge, encode_ans)
        decode_fbii = self._decode_response(challenge, encode_fbii)
        decode_igi = self._decode_response(challenge, encode_igi)
        validate_result = self._validate_fail_image(
            decode_ans, decode_fbii, decode_igi)
        return validate_result

    def _check_para(self, challenge, validate, seccode):
        return (bool(challenge.strip()) and bool(validate.strip()) and bool(seccode.strip()))

    def _validate_fail_image(self, ans, full_bg_index, img_grp_index):
        thread = 3
        full_bg_name = str(self._md5_encode(str(full_bg_index)))[0:10]
        bg_name = str(self._md5_encode(str(img_grp_index)))[10:20]
        answer_decode = ""
        for i in range(0, 9):
            if i % 2 == 0:
                answer_decode += full_bg_name[i]
            elif i % 2 == 1:
                answer_decode += bg_name[i]
        x_decode = answer_decode[4:]
        x_int = int(x_decode, 16)
        result = x_int % 200
        if result < 40:
            result = 40
        if abs(ans - result) < thread:
            return 1
        else:
            return 0

    def _md5_encode(self, values):
        if type(values) == str:
            values = values.encode()
        m = md5(values)
        return m.hexdigest()

    def _decode_rand_base(self, challenge):
        str_base = challenge[32:]
        i = 0
        temp_array = []
        for i in xrange(len(str_base)):
            temp_char = str_base[i]
            temp_ascii = ord(temp_char)
            result = temp_ascii - 87 if temp_ascii > 57 else temp_ascii - 48
            temp_array.append(result)
        decode_res = temp_array[0] * 36 + temp_array[1]
        return decode_res

    def _decode_response(self, challenge, userresponse):
        if len(userresponse) > 100:
            return 0
        shuzi = (1, 2, 5, 10, 50)
        chongfu = set()
        key = {}
        count = 0
        for i in challenge:
            if i in chongfu:
                continue
            else:
                value = shuzi[count % 5]
                chongfu.add(i)
                count += 1
                key.update({i: value})
        res = 0
        for i in userresponse:
            res += key.get(i, 0)
        res = res - self._decode_rand_base(challenge)
        return res

### utils/captcha_verify.py
from django.conf import settings
from api.utils.geetest import GeeTestLib


def verify(verify_data, uid=None, extend_params=None):
    """第三方滑动验证码校验.

    选用第三方的验证组件, 根据参数进行校验
    根据布尔值辨别是否校验通过

    Parameters
    ----------

    verify_data : dict
        请求数据

    uid: string, default: None
        用户UID, 如果存在就免受滑动验证码的限制

    extend_params : dict
        预留的扩展参数

    Returns
    -------
    True OR False
    """

    captcha_config = settings.GEE_TEST

    if captcha_config.get("verify_status"):

        status = True

        if uid in captcha_config.get("not_verify"):
            return True

        gt = GeeTestLib(captcha_config["gee_test_access_id"], captcha_config["gee_test_access_key"])
        challenge = verify_data.get(gt.FN_CHALLENGE, '')
        validate = verify_data.get(gt.FN_VALIDATE, '')
        seccode = verify_data.get(gt.FN_SECCODE, '')
        # status = request.session.get(gt.GT_STATUS_SESSION_KEY, 1)
        # user_id = request.session.get("user_id")

        if status:
            result = gt.success_validate(challenge, validate, seccode, None)
            else:
                result = gt.failback_validate(challenge, validate, seccode)
                return True if result else False
            else:
                return True
            
### views/captcha.py(前端页面加载完毕,首次发起请求,获取'滑块验证'所需要的json参数)[也可以把这边逻辑写到LoginView的get方法中(节省url)]
# 极验测试: url(r'^captcha/$', views.CaptchaView.as_view()),
import json

from rest_framework.views import APIView
from api.utils.geetest import GeeTestLib
from rest_framework.response import Response

from django.conf import settings


class CaptchaView(APIView):

    def get(self, request):
        gt = GeeTestLib(settings.GEE_TEST["gee_test_access_id"], settings.GEE_TEST["gee_test_access_key"])
        gt.pre_process()
        # 设置 geetest session, 用于是否启用滑动验证码向 geetest 发起远程验证, 如果取不到的话只是对本地轨迹进行校验
        # self.request.session[gt.GT_STATUS_SESSION_KEY] = status
        # request.session["user_id"] = user_id
        response_str = gt.get_response_str()
        response_str = json.loads(response_str)

        return Response({"error_no": 0, "data": response_str})

  • settings配置
......
GEE_TEST = {
    "gee_test_access_id": "your_gee_test_access_id",
    "gee_test_access_key": "your_gee_test_access_key",
    "verify_status": True,  # 是否启用滑动验证码验证组件(True表示启用)
    "not_verify": [
        "xxxxxxxxxxxxxxxxxx", # 不用验证的用户(存放着用户的uid)
    ],  
}
  • get请求测试结果
- 测试URL: http://127.0.0.1:8000/captcha/
- 返回结果:
	{
        "error_no": 0,
        "data": {
            "success": 1,
            "gt": "b8a8ea14f2b000253462b3a0590076ba",
            "challenge": "8a9de1d5abb04734bee8d3ac0d98da9c",
            "new_captcha": true
        }
    }

  • post请求(简单演示,实际需求应该写到LoginView里面): 校验verify(request.data)的返回值True或者False
    • 若判断结果是True,则继续后面的登录校验逻辑
    • False: 立即触发异常,返回错误消息给前端,登录逻辑到此为止
from utils.jiyanDemo.first_method.captcha_verify import verify
......
#  url(r'^login_jiyan_demo/$', views.LoginJiyanDemoView.as_view()),
class LoginJiyanDemoView(APIView):
    def post(self,request):
    	# 校验前端传过来的校验码参数是否正确(实际需求中,正确就继续登录逻辑,不正确就立即返回错误消息)
        is_valid = verify(request.data)
        print(request.data) # {'geetest_challenge': '864f4b47db92c0bc17b1a4a77d840dda4j', 'geetest_seccode': 'f2ddba6aa4743d357c4aeee74c6f8a77|jordan', 'geetest_validate': 'f2ddba6aa4743d357c4aeee74c6f8a77'}
        print('验证的结果为: ',is_valid) # 验证的结果为:  True
        return Response({'msg':'响应请求成功,结果为: {}'.format(is_valid)})

前端部分(使用vue来演示)

  • src/utils目录下面保存gt.js(官方提供的SDK)
// gt.js

"v0.4.8 Geetest Inc.";

(function (window) {
    "use strict";
    if (typeof window === 'undefined') {
        throw new Error('Geetest requires browser environment');
    }

var document = window.document;
var Math = window.Math;
var head = document.getElementsByTagName("head")[0];

function _Object(obj) {
    this._obj = obj;
}

_Object.prototype = {
    _each: function (process) {
        var _obj = this._obj;
        for (var k in _obj) {
            if (_obj.hasOwnProperty(k)) {
                process(k, _obj[k]);
            }
        }
        return this;
    }
};

function Config(config) {
    var self = this;
    new _Object(config)._each(function (key, value) {
        self[key] = value;
    });
}

Config.prototype = {
    api_server: 'api.geetest.com',
    protocol: 'http://',
    typePath: '/gettype.php',
    fallback_config: {
        slide: {
            static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"],
            type: 'slide',
            slide: '/static/js/geetest.0.0.0.js'
        },
        fullpage: {
            static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"],
            type: 'fullpage',
            fullpage: '/static/js/fullpage.0.0.0.js'
        }
    },
    _get_fallback_config: function () {
        var self = this;
        if (isString(self.type)) {
            return self.fallback_config[self.type];
        } else if (self.new_captcha) {
            return self.fallback_config.fullpage;
        } else {
            return self.fallback_config.slide;
        }
    },
    _extend: function (obj) {
        var self = this;
        new _Object(obj)._each(function (key, value) {
            self[key] = value;
        })
    }
};
var isNumber = function (value) {
    return (typeof value === 'number');
};
var isString = function (value) {
    return (typeof value === 'string');
};
var isBoolean = function (value) {
    return (typeof value === 'boolean');
};
var isObject = function (value) {
    return (typeof value === 'object' && value !== null);
};
var isFunction = function (value) {
    return (typeof value === 'function');
};
var MOBILE = /Mobi/i.test(navigator.userAgent);
var pt = MOBILE ? 3 : 0;

var callbacks = {};
var status = {};

var nowDate = function () {
    var date = new Date();
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    var day = date.getDate();
    var hours = date.getHours();
    var minutes = date.getMinutes();
    var seconds = date.getSeconds();

    if (month >= 1 && month <= 9) {
      month = '0' + month;
    }
    if (day >= 0 && day <= 9) {
      day = '0' + day;
    }
    if (hours >= 0 && hours <= 9) {
      hours = '0' + hours;
    }
    if (minutes >= 0 && minutes <= 9) {
      minutes = '0' + minutes;
    }
    if (seconds >= 0 && seconds <= 9) {
      seconds = '0' + seconds;
    }
    var currentdate = year + '-' + month + '-' + day + " " + hours + ":" + minutes + ":" + seconds;
    return currentdate;
}

var random = function () {
    return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};

var loadScript = function (url, cb) {
    var script = document.createElement("script");
    script.charset = "UTF-8";
    script.async = true;

    // 对geetest的静态资源添加 crossOrigin
    if ( /static\.geetest\.com/g.test(url)) {
        script.crossOrigin = "anonymous";
    }

    script.onerror = function () {
        cb(true);
    };
    var loaded = false;
    script.onload = script.onreadystatechange = function () {
        if (!loaded &&
            (!script.readyState ||
            "loaded" === script.readyState ||
            "complete" === script.readyState)) {

            loaded = true;
            setTimeout(function () {
                cb(false);
            }, 0);
        }
    };
    script.src = url;
    head.appendChild(script);
};

var normalizeDomain = function (domain) {
    // special domain: uems.sysu.edu.cn/jwxt/geetest/
    // return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
    return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
};
var normalizePath = function (path) {
    path = path.replace(/\/+/g, '/');
    if (path.indexOf('/') !== 0) {
        path = '/' + path;
    }
    return path;
};
var normalizeQuery = function (query) {
    if (!query) {
        return '';
    }
    var q = '?';
    new _Object(query)._each(function (key, value) {
        if (isString(value) || isNumber(value) || isBoolean(value)) {
            q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
        }
    });
    if (q === '?') {
        q = '';
    }
    return q.replace(/&$/, '');
};
var makeURL = function (protocol, domain, path, query) {
    domain = normalizeDomain(domain);

    var url = normalizePath(path) + normalizeQuery(query);
    if (domain) {
        url = protocol + domain + url;
    }

    return url;
};

var load = function (config, send, protocol, domains, path, query, cb) {
    var tryRequest = function (at) {

        var url = makeURL(protocol, domains[at], path, query);
        loadScript(url, function (err) {
            if (err) {
                if (at >= domains.length - 1) {
                    cb(true);
                    // report gettype error
                    if (send) {
                        config.error_code = 508;
                        var url = protocol + domains[at] + path;
                        reportError(config, url);
                    }
                } else {
                    tryRequest(at + 1);
                }
            } else {
                cb(false);
            }
        });
    };
    tryRequest(0);
};


var jsonp = function (domains, path, config, callback) {
    if (isObject(config.getLib)) {
        config._extend(config.getLib);
        callback(config);
        return;
    }
    if (config.offline) {
        callback(config._get_fallback_config());
        return;
    }

    var cb = "geetest_" + random();
    window[cb] = function (data) {
        if (data.status == 'success') {
            callback(data.data);
        } else if (!data.status) {
            callback(data);
        } else {
            callback(config._get_fallback_config());
        }
        window[cb] = undefined;
        try {
            delete window[cb];
        } catch (e) {
        }
    };
    load(config, true, config.protocol, domains, path, {
        gt: config.gt,
        callback: cb
    }, function (err) {
        if (err) {
            callback(config._get_fallback_config());
        }
    });
};

var reportError = function (config, url) {
    load(config, false, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
        time: nowDate(),
        captcha_id: config.gt,
        challenge: config.challenge,
        pt: pt,
        exception_url: url,
        error_code: config.error_code
    }, function (err) {})
}

var throwError = function (errorType, config) {
    var errors = {
        networkError: '网络错误',
        gtTypeError: 'gt字段不是字符串类型'
    };
    if (typeof config.onError === 'function') {
        config.onError(errors[errorType]);
    } else {
        throw new Error(errors[errorType]);
    }
};

var detect = function () {
    return window.Geetest || document.getElementById("gt_lib");
};

if (detect()) {
    status.slide = "loaded";
}

window.initGeetest = function (userConfig, callback) {

    var config = new Config(userConfig);

    if (userConfig.https) {
        config.protocol = 'https://';
    } else if (!userConfig.protocol) {
        config.protocol = window.location.protocol + '//';
    }

    // for KFC
    if (userConfig.gt === '050cffef4ae57b5d5e529fea9540b0d1' ||
        userConfig.gt === '3bd38408ae4af923ed36e13819b14d42') {
        config.apiserver = 'yumchina.geetest.com/'; // for old js
        config.api_server = 'yumchina.geetest.com';
    }

    if(userConfig.gt){
        window.GeeGT = userConfig.gt
    }

    if(userConfig.challenge){
        window.GeeChallenge = userConfig.challenge
    }

    if (isObject(userConfig.getType)) {
        config._extend(userConfig.getType);
    }
    jsonp([config.api_server || config.apiserver], config.typePath, config, function (newConfig) {
        var type = newConfig.type;
        var init = function () {
            config._extend(newConfig);
            callback(new window.Geetest(config));
        };

        callbacks[type] = callbacks[type] || [];
        var s = status[type] || 'init';
        if (s === 'init') {
            status[type] = 'loading';

            callbacks[type].push(init);

            load(config, true, config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function (err) {
                if (err) {
                    status[type] = 'fail';
                    throwError('networkError', config);
                } else {
                    status[type] = 'loaded';
                    var cbs = callbacks[type];
                    for (var i = 0, len = cbs.length; i < len; i = i + 1) {
                        var cb = cbs[i];
                        if (isFunction(cb)) {
                            cb();
                        }
                    }
                    callbacks[type] = [];
                }
            });
        } else if (s === "loaded") {
            init();
        } else if (s === "fail") {
            throwError('networkError', config);
        } else if (s === "loading") {
            callbacks[type].push(init);
        }
    });

};


})(window);


  • main.js中引入上面的gt.js
......
// 导入极验js模块
import '@/utils/gt'
  • login.vue登录界面的主要逻辑
- 第一次的get请求获取校验码参数,保存起来: /captcha/
- 带着校验码参数,发第二次的post请求: /login_jiyan_demo/
- 在Login.vue组件中的 created 中写上  this.getGeetest();  ,让登录组件创建完成时就向服务端发送get请求得到初始化验证界面
- get请求成功以后,后端返回3个重要参数(用于二次验证,post请求的时候会用到)保存起来
- 调用SDK的initGeetest(配置项,function),
	- 传入配置项
		- 传入后端返回的三个校验参数(必须)
		- 验证组件的其他配置项可以看官网,根据自身需求配置(非必须)
	- 传入function
		- 把(captchaObj.appendTo("#geetest")将验证组件放在页面中id="geetest"的标签中
			- 例如结构中可以这么写: <div id="geetest"></div>)
		- 存储 captchaObj.getValidate() 对象
			- 例如: var result = captchaObj.getValidate();this.geetestObj = result
	
### login.index.vue
<template>
	......
	<!--校验组件的结构-->
	<el-form-item>
        <div id="geetest"></div>
    </el-form-item>
    ......
    <!--极验测试按钮,正常情况下,写在login登录按钮即可-->
    <el-button :loading="loading" type="warning" style="width:100%;margin-bottom:30px;" @click.native.prevent="postJiyanDemoData">极验测试</el-button>
</template>

<script>
......
import request from '@/utils/request'
// 配置get请求和post请求路径(简写,就不放到vuex或者绑定$API了)
var reqGetVerify = ()=>request({
  url:'/captcha/',
  method:'get',
  responseType:'json'
})

var reqPostVerify = (data)=>request({
  url:'/login_jiyan_demo/',
  method:'post',
  responseType:'json',
  data,
})

export default {
	data() {
        return {
         ......
         geetestObj:{}, // 保存极验对象
         jiyanSecondVerify:false // 滑块验证结果是否成功的标志
        }
  	},
  	methods:{
  		......
  		// 极验demo之get请求
        async getJiyanDemoData(){
          let res = await reqGetVerify()
          let data = res.data
          initGeetest(
            {
              // 后端的返回值+文档配置项
              gt: data.gt,
              challenge: data.challenge,
              offline: !data.success,
              new_captcha: true,
              width: "100%"
            },
            (captchaObj)=> {
              // 把验证码组件加载到预留的html结构中
              captchaObj.appendTo("#geetest");
              // 存储后端的返回值
              captchaObj.onSuccess(()=> {
                var result = captchaObj.getValidate();
                this.geetestObj = result;
                this.jiyanSecondVerify = true // 表示滑块验证成功
              });
            }
          );
        },
        // 极验demo之post请求
        async postJiyanDemoData(){
          if(!this.jiyanSecondVerify){ // 优先校验滑块验证是否通过
          this.$message({
            type:'warning',
            message:'请完成验证!'
          })
          return
        }
          // 打包存储的数据
          let data = {
            geetest_challenge: this.geetestObj.geetest_challenge,
            geetest_seccode: this.geetestObj.geetest_seccode,
            geetest_validate: this.geetestObj.geetest_validate,
          }
          // 发post请求
          let res = await reqPostVerify(data)
          // {msg: "响应请求成功,结果为: True"}
          console.log(res)
        }
  	},
  	mounted(){ // 调用(这里也可以写在created())
        this.getJiyanDemoData()
      }
}