21
使用vue、react的项目获取数据、上传数据、注册、登陆等都是通过接口来完成的,接口很容易被人恶意调用,最容易被人恶意调用的接口就是注册、登陆类的接口,为了防止接口被恶意调用很多公司开发了出了很多的人机验证的工具,今天就来讲下极验验证在vue项目中的使用。

效果预览
极验2.gif

1、遇到的问题

  1. 在项目中任何一个页面或vue组件都有可能需要使用到极验,并且极验在初始化时需要传递一些参数,要怎么做才能做到每一个组件都能很方便的使用极验呢?
  2. 极验应该在什么时候初始化?是组件一加载就初始化还是用户点击按钮后再初始化?
  3. 在多语言项目中,用户手动切换语言后的极验重置

2、代码分享

为了在多个页面或多个组件中都能方便的使用极验,我将极验初始化的相关代码写到了mixins中。这样做的好处是:方便在组件中获取极验相关的数据,以及调用极验相关api,如做重置、销毁等操作;缺点是:在同一个页面中无法在多个地方使用mixin,但这也是有解决方案的。

geetest-mixin.js

/*
  极验mixin
 */
// 导入极验官方给的代码
import gt from "../common/js/geetest/geetest.gt";
import {commonApi} from "../api/commonApi";
import {mapGetters} from "vuex";
// 自定义语言与极验语言对应表
const geetestLangMap = {
  "zh_CN": "zh-cn",
  "zh_TW": "zh-tw",
  "en_US": "en",
  "ja_JP": "ja",
  "ko_KR": "ko",
  "ru_RU": "ru"
};
console.log('gt',gt)
// 极验默认配置
const geetestOptions = {
  product: 'popup', // 极验展现形式 可选值有 float、popup、custom、bind
  width: '100%',
  lang: 'zh_CN',
  autoShow: true, // 当product为bind时,如果次参数为true,则在极验加载完成后立即显示极验弹窗
  area: null, // 极验绑定的元素,仅在 product为 custom、float、popup时需要传递
  autoRefreshOnLangChange: true, // 语言改变时是否自动刷新
};
export const geetestMixin = {
  data(){
    return {
      geetest: {
        geetestSuccessData: null, // 极验用户行为操作成功后的数据
        geetestObj: null, // 极验对象
        geetestLoading: false,
        geetestFatched: false, // 判断是否从服务器获取了极验数据
        showGeetest: false, // 是否使用极验
        sign: "", // 极验降级 用的签名
        geetestRestartCountMax: 5, // 极验重试最大此时
        count: 1,
        geetestOptions: {}
      }
    }
  },
  computed: {
    ...mapGetters(['get_lang'])
  },
  watch: {
    get_lang(lang){
      let options = this.geetest.geetestOptions;
      if(options.autoRefreshOnLangChange && this.geetest.geetestObj){
        this.initGeetest({
          ...options,
          lang: lang.code
        });
      }
    }
  },
  methods: {
    // 初始化极验
    initGeetest(options){
      if(!options || ({}).toString.call(options) !== '[object Object]'){
        console.error('initGeetest方法的参数options必须是一个对象!');
        return;
      }
      let newOptions = Object.assign({}, geetestOptions, options);
      if((newOptions.product === 'popup' || newOptions.product === 'custom' || newOptions.product === 'float') && !newOptions.area){
        console.error('product为popup、custom、float时options参数中必须有area属性,area属性值可以为css选择器或dom元素!');
        return;
      }
      this.geetest.geetestOptions = newOptions;
      this._geetestRegist_(newOptions);
    },
    // 重置极验
    geetestReset(cb){
      if(this.geetest.geetestObj){
        this.geetest.geetestSuccessData = {};
        this.geetest.geetestObj.reset();
        if(typeof cb === 'function'){
          cb();
        }
      }else{
        console.error('极验不存在!');
      }
    },
    // 显示极验弹窗,此方法只有在product为bind时才有效
    geetestShow(){
      if(this.geetest.geetestObj){
        if(this.geetest.geetestOptions.product === 'bind'){
          this.geetest.geetestObj.verify();
        }else{
          console.error('极验的product值非bind,无法调用show!');
        }
      }else{
        console.error('极验不存在!');
      }
    },
    // 初始化极验,内部使用
    _initGeetestInternal_(data, options){
      let that = this;
      let geetest = this.geetest;

      window.initGeetest({
        // 以下 4 个配置参数为必须,不能缺少
        gt: data.gt,
        challenge: data.challenge,
        offline: !data.success, // 表示用户后台检测极验服务器是否宕机
        new_captcha: true, // 用于宕机时表示是新验证码的宕机
        product: options.product, // 产品形式,包括:float,popup,bind
        width: options.width,
        lang: geetestLangMap[options.lang]
      }, function (captchaObj) {
                if(geetest.geetestObj){
                  try {
                    // 如果之前已经初始化过了,则线将之前生成的dom移除掉
                    geetest.geetestObj.destroy();
                  }catch (e) {
                    console.error('极验销毁失败', e);
                  }
                }
        geetest.geetestObj = captchaObj;
        if((options.product === 'popup' || options.product === 'custom' || options.product === 'float')){
          captchaObj.appendTo(options.area);
        }
        // 为bind模式时极验加载完成后自动弹出极验弹窗
        if(options.autoShow && options.product === 'bind'){
          captchaObj.onReady(() => {
            captchaObj.verify();
          });
        }
        geetest.geetestSuccessData = {};
        // 当用户操作后并且通过验证后的回调
        captchaObj.onSuccess(function () {
          let successData = captchaObj.getValidate();
          geetest.geetestSuccessData = successData;
          console.log('用户行为验证通过数据', successData);
          /*
            这种方式不可采用,原因,作用域会被缓存
            if (typeof options.callback === 'function') {
              options.callback(successData);
            }
            用户行为验证通过后调用回调函数
          */
          if(typeof that.onGeetestSuccess === 'function'){
            that.onGeetestSuccess(successData);
          }
        });
        captchaObj.onError(function (e) {
          console.error("极验出错了", e);
        });
        console.log('极验实例', captchaObj);
      });
    },
    // 调用接口,获取极验数据
    _geetestRegist_(options){
      if(this.geetest.geetestLoading){
        return;
      }
      this.geetest.geetestLoading = true;
      commonApi.getGtCaptcha()
        .then(res => {
          let data = res.data;
          // TIP 后台需要控制是否开启极验,因此需要判断 enable===true && success===1 才显示极限
          this.geetest.sign = data.sign;
          this.geetest.geetestFatched = true;
          if(typeof data.enable == "undefined" || (data.enable === true && data.success === 1)) {
            this.geetest.showGeetest = true;
          }else{
            this.geetest.showGeetest = false;
            this.geetest.geetestLoading = false;
            /*// 如果极验禁用,则调用onDisableGeetest回调
            if(typeof options.onDisableGeetest === 'function'){
              options.onDisableGeetest();
            }*/
            // 如果极验禁用,则调用onDisableGeetest回调
            if(typeof this.onDisableGeetest === 'function'){
              this.onDisableGeetest();
            }
            return
          }
          this.geetest.geetestLoading = false;
          this._initGeetestInternal_(data, options);
        })
        .catch((err) => {
          console.error('极验初始化失败', err);
          if(this.geetest.count > this.geetest.geetestRestartCountMax){
            this.geetest.geetestLoading = false;
            return;
          }
          console.log('正在重试初始化极验!当前次数:' + this.geetest.count);
          this.geetest.count++;
          this._geetestRegist_(options);
        });
    }
  },
  beforeDestroy(){
    if(this.geetest.geetestObj){
      this.geetest.geetestObj.destroy();
    }
  }
};

geetest.gt.js

段代码可以不用关注,极验官网有
/* initGeetest 1.0.0
 * 用于加载id对应的验证码库,并支持宕机模式
 * 暴露 initGeetest 进行验证码的初始化
 * 一般不需要用户进行修改
 */
var gtInit = (function (global, factory) {
  "use strict";
  if (typeof module === "object" && typeof module.exports === "object") {
    // CommonJS
    module.exports = global.document ?
      factory(global, true) :
      function (w) {
        if (!w.document) {
          throw new Error("Geetest requires a window with a document");
        }
        return factory(w);
      };
  } else {
    factory(global);
  }
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
  "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://',
    type_path: '/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 callbacks = {};
  var status = {};
  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;
    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) {
    return domain.replace(/^https?:\/\/|\/$/g, '');
  };
  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 (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);
          } 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.protocol, domains, path, {
      gt: config.gt,
      callback: cb
    }, function (err) {
      if (err) {
        callback(config._get_fallback_config());
      }
    });
  };
  var throwError = function (errorType, config) {
    var errors = {
      networkError: '网络错误'
    };
    if (typeof config.onError === 'function') {
      config.onError(errors[errorType]);
    } else {
      throw new Error(errors[errorType]);
    }
  };
  var detect = function () {
    return !!window.Geetest;
  };
  if (detect()) {
    status.slide = "loaded";
  }
  var initGeetest = function (userConfig, callback) {
    var config = new Config(userConfig);
    if (userConfig.https) {
      config.protocol = 'https://';
    } else if (!userConfig.protocol) {
      config.protocol = window.location.protocol + '//';
    }
    jsonp([config.api_server || config.apiserver], config.type_path, 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.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.initGeetest = initGeetest;
  return initGeetest;
});

export default {
  gtInit
}

页面中使用

// 导入极验验证
import {geetestMixin} from "./geetest-mixin";
import {mapGetters} from "vuex";
import {commonApi} from "../api/commonApi";

export default {
  name: 'Regist',
  mixins: [geetestMixin],
  data(){
    return {
      form: {
        ...表单数据
      },
      committing: false,
      errMsg: ' ',.
    }
  },
  methods: {
    // 提交注册数据
    submitRegistData(){
      ...你的业务逻辑
      /*if(this.geetest.showGeetest){
        // 如果没有geetest_challenge,则说明用户没有进行行为验证
        if(!this.geetest.geetestSuccessData.geetest_challenge){
          this.errMsg = this.$t('formError.geetest'); // 点击按钮进行验证
          return;
        }
      }*/
      this.errMsg = '';


      if(!this.geetest.geetestObj){
        /*
          如果this.geetest.geetestFatched==true,则说明已经加载过极验了
          如果this.geetest.showGeetest==false,则说明后台关闭极验了
         */
        if(this.geetest.geetestFatched && !this.geetest.showGeetest){
          //this.committing = true;
          this.commitData();
        }else{
          this.initGeetest({
            product: 'bind',
            lang: this.get_lang.code,
          });
        }
      }else{
        if(this.geetest.showGeetest){
          this.geetestShow();
        }else{
          console.log('fasfsafsdfsd')
          //this.committing = true;
          this.commitData();
        }
      }
    },
    // 提交数据
    commitData(){
      if(this.committing){
        return;
      }
      this.committing = true;
      let data = {
        ...this.form
      };
      let geetestData = {};
      // 获取极验数据
      if(this.geetest.showGeetest){
        geetestData = {
          ...this.geetest.geetestSuccessData,
          sign: this.geetest.sign
        }
      }
      if(!this.form.inviteCode){
        delete data.inviteCode;
      }
      commonApi.regist(data, geetestData)
        .then(res => {
          this.committing = false;
          if(res.errcode === 0){
            ...你的业务逻辑
          }else{
          // 重置极验,使极验回到可操作状态
            this.geetestReset();
          }
        })
        .catch(() => {
          this.committing = false;
          // 重置极验,使极验回到可操作状态
          this.geetestReset();
        });
    },
    // 极验验证成功后回调
    onGeetestSuccess(){
      // 用户通过极验行为验证后做的操作
      this.commitData();
    },
    // 极验被禁用后回调
    onDisableGeetest(){
      this.commitData();
    }
  },
  computed: {
    ...mapGetters(['get_lang'])
  }
};

3、极验初始化时间问题

geetest-mixin.js设计的比较灵活,它可以允许你在任何时机初始化极验。但在项目中推荐在需要使用到的时候再初始化,1来可以节省流量;2来可以提升页面性能;3是最重要的一个点,在单页面应用程序中都是通过接口来访问数据的,而接口都有过期时间,如果组件初始化完成就立即初始化极验,而用户在接口过期后再去操作则会出现一些奇葩的bug

4、多语言项目中用户手动切换语言的问题

由于单页面应用程序切换语言后页面不会刷新,所以就会出现页面语言已经切换了,但极验还是使用的原来的语言。我的解决方案就是在用户切换语言后手动的刷新一下极验

{
  watch: {
    get_lang(lang){
      let options = this.geetest.geetestOptions;
      // 如果开启了语言切换自手动刷新极验,并且极验已经初始化了则刷新。如果极验都还没有初始化则可以不用去刷新
      if(options.autoRefreshOnLangChange && this.geetest.geetestObj){
        this.initGeetest({
          ...options,
          lang: lang.code
        });
      }
    }
  }
}

5、关于点击按钮时按钮loading效果的控制

如《效果预览》图中的获取验证码loading效果控制,可以通过geetest.geetestLoading来进行判断


heath_learning
1.4k 声望31 粉丝