极验使用详情见官网:https://docs.geetest.com/install/deploy/server/python
后端 Django 使用极验方式:
1.准备好3个文件,并放在对应位置:
# !/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
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
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})
2.在settings中配置geetest需要的参数:
GEE_TEST = { "gee_test_access_id": "37ca5631edd1e882721808d35163b3ad", "gee_test_access_key": "7eb11ccf3e0953bdd060ed8b60b0c4f5", "verify_status": True, # 是否启用滑动验证码验证组件(True表示启用) "not_verify": [ "2ba6b08d53a4fd27057a32537e2d55ae", ], # 不用验证的用户(存放着用户的uid) }
3.在urls.py中配置前端要请求的极验路径:
from django.contrib import admin from django.urls import path from api.views.login import LoginView from api.views.captcha import CaptchaView urlpatterns = [ path('admin/', admin.site.urls), path('login/', LoginView.as_view()), # 极验geetest path("api/captcha_check/", CaptchaView.as_view()), ]
4.在登录视图中调用 utils/captcha_verify.py 中的 verify,通过判断 verify(request.data) 是否为True 来决定用户能否登录成功,验证错误返回错误信息:
from django.contrib import auth import uuid import datetime from rest_framework.views import APIView from rest_framework.response import Response from api.models import Token from api.utils.captcha_verify import verify class LoginView(APIView): def post(self, request): res = {'user': None, 'msg': None} is_valid = verify(request.data) try: if is_valid: user = request.data.get('user') pwd = request.data.get('pwd') user_obj = auth.authenticate(username=user, password=pwd) if user_obj: random_str = str(uuid.uuid4()) Token.objects.update_or_create(user=user_obj, defaults={"key": random_str, 'created': datetime.datetime.now()}) res['user'] = user_obj.username res['token'] = random_str else: res['msg'] = 'user or pwd error' else: res['msg'] = '验证码异常!' except Exception as e: res['msg'] = str(e) return Response(res)
前端 vue 使用(配合后端,主要从后端geetest得到3个重要参数:challenge,validate,seccode)极验方式:
1.先下载gt.js并放在global下:
"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);
2.在main.js中引入全局的gt.js:
// 引入全局的geetest.js import '../static/global/gt'
import Vue from 'vue' import App from './App' import router from './router' //1.导入element-ui模块 并且导入全局的css样式 import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); //将lfHeader 注册成全局组件 import LfHeader from '@/components/Common/LfHeader' Vue.component(LfHeader.name,LfHeader); // 引入vue-cookies import VueCookies from 'vue-cookies' Vue.use(VueCookies); // 引入全局的geetest.js import '../static/global/gt' //引入项目中的全局的css样式 import '../static/global/index.css' import * as api from './restful/api' Vue.prototype.$http = api; //导入store实例 import store from './store' //全局导航守卫 router.beforeEach((to, from, next) => { if(VueCookies.isKey('access_token')){ let user = { username:VueCookies.get('username'), shop_cart_num:VueCookies.get('shop_cart_num'), access_token:VueCookies.get('access_token'), avatar:VueCookies.get('avatar'), notice_num:VueCookies.get('notice_num') }; store.dispatch('getUser',user) } next() }); Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ el: '#app', router, store, components: { App }, template: '<App/>' });
3.在Login.vue组件中的 created 中写上 this.getGeetest(); ,让登录组件创建完成时就向服务端发送axios请求得到初始化验证界面(captchaObj.appendTo("#geetest")将验证组件放在页面中id="geetest"的标签中,如<div id="geetest"></div>)和3个重要参数(用于二次验证),然后用户点击登录按钮时触发 loginHandler 事件,完成二次验证(注:axios请求都放在 src/restful/api.js 中)
<template> <div class="box"> <img src="https://www.luffycity.com/static/img/Loginbg.3377d0c.jpg" alt> <div class="login"> <div class="login-title"> <img src="https://www.luffycity.com/static/img/Logotitle.1ba5466.png" alt> <p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p> </div> <div class="login_box"> <div class="title"> <span>密码登录</span> <span>短信登录</span> </div> <div class="inp"> <input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user"> <input v-model="password" type="password" name class="pwd" placeholder="密码"> <div id="geetest"></div> <div class="rember"> <p> <input type="checkbox" class="no" name="a"> <span>记住密码</span> </p> <p>忘记密码</p> </div> <button class="login_btn" @click="loginHandler">登录</button> <p class="go_login"> 没有账号 <span>立即注册</span> </p> </div> </div> </div> </div> </template> <script> export default { name: "Login", data() { return { username: "", password: "", geetestObj:{} }; }, methods: { loginHandler(){ if(!this.geetestObj){ return; }else{ //登录状态 let params = { geetest_challenge: this.geetestObj.geetest_challenge, geetest_seccode: this.geetestObj.geetest_seccode, geetest_validate: this.geetestObj.geetest_validate, username:this.username, password:this.password } this.$http.login(params) .then(res=>{ if(res.error_no === 0){ //前面的记录 this.$router.go(-1) let data = res.data; //存储cookie 通过for-in循环来遍历 字典对象 for(let key in data){ this.$cookies.set(key,data[key]) } //分发actions中声明的方法 this.$store.dispatch('getUser',data); } }) .catch(err=>{ console.log(err) }) } }, // getGeetest() { this.$http .geetest() .then(res => { let data = res.data; initGeetest( { // 以下配置参数来自服务端 SDK gt: data.gt, challenge: data.challenge, offline: !data.success, new_captcha: true, width: "100%" }, (captchaObj)=> { // 这里可以调用验证实例 captchaObj 的实例方法 captchaObj.appendTo("#geetest"); // 这里调用了 onSuccess 方法 captchaObj.onSuccess(()=> { var result = captchaObj.getValidate(); this.geetestObj = result }); } ); }) .catch(err => { console.log(err); }); } }, created() { this.getGeetest(); } }; </script> <style lang="css" scoped> .box { width: 100%; position: relative; } .box img { width: 100%; } .box .login { position: absolute; width: 500px; height: 400px; top: 50%; left: 50%; margin-left: -250px; margin-top: -300px; } .login .login-title { width: 100%; text-align: center; } .login-title img { width: 190px; height: auto; } .login-title p { font-family: PingFangSC-Regular; font-size: 18px; color: #fff; letter-spacing: 0.29px; padding-top: 10px; padding-bottom: 50px; } .login_box { width: 400px; height: auto; background: #fff; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5); border-radius: 4px; margin: 0 auto; padding-bottom: 40px; } .login_box .title { font-size: 20px; color: #9b9b9b; letter-spacing: 0.32px; border-bottom: 1px solid #e6e6e6; display: flex; justify-content: space-around; padding: 50px 60px 0 60px; margin-bottom: 20px; cursor: pointer; } .login_box .title span:nth-of-type(1) { color: #4a4a4a; border-bottom: 2px solid #84cc39; } .inp { width: 350px; margin: 0 auto; } .inp input { border: 0; outline: 0; width: 100%; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp input.user { margin-bottom: 16px; } .inp .rember { display: flex; justify-content: space-between; align-items: center; position: relative; margin-top: 10px; } .inp .rember p:first-of-type { font-size: 12px; color: #4a4a4a; letter-spacing: 0.19px; margin-left: 22px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; /*position: relative;*/ } .inp .rember p:nth-of-type(2) { font-size: 14px; color: #9b9b9b; letter-spacing: 0.19px; cursor: pointer; } .inp .rember input { outline: 0; width: 15px; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp .rember p span { display: inline-block; font-size: 12px; width: 100px; /*position: absolute;*/ /*left: 20px;*/ } #geetest { margin-top: 20px; } .login_btn { width: 100%; height: 45px; background: #84cc39; border-radius: 5px; font-size: 16px; color: #fff; letter-spacing: 0.26px; margin-top: 30px; } .inp .go_login { text-align: center; font-size: 14px; color: #9b9b9b; letter-spacing: 0.26px; padding-top: 20px; } .inp .go_login span { color: #84cc39; cursor: pointer; } </style>
import Axios from 'axios' import VueCookies from 'vue-cookies' Axios.defaults.baseURL='http://127.0.0.1:8000/'; const captchaCheckUrl = 'api/captcha_check/'; const loginUrl = 'login/'; //请求拦截器 Axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 if(VueCookies.isKey('access_token')){ //有问题? config.headers.Authorization = VueCookies.get('access_token'); } return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); }); export function geetest(){ return Axios.get(`${captchaCheckUrl}`).then(res=>res.data) } export function login(params){ return Axios.post(`${loginUrl}`,params).then(res=>res.data) }