GeeTest in React

2,217 阅读4分钟

GeeTest行为验证

极验「行为验证」是一项可以帮助你的网站与APP识别与拦截机器程序批量自动化操作的SaaS应用。它是由极验开发的新一代人机验证产品,它不基于传统“问题-答案”的检测模式,而是通过利用深度学习对验证过程中产生的行为数据进行高维分析,发现人机行为模式与行为特征的差异,更加精准地区分人机行为。

使用场景

网站和APP,在所有可能被机器行为攻击的场景,例如但不限于注册、登录、短信接口、查询接口、营销活动、发帖评论等等,都可以部署使用「行为验证」,来抵御机器批量操作。

GeeTest in React

1、引入 gt.js

这一段代码可以直接在官网复制,我这边是有进行细微的语法调整。

/* eslint-disable radix */
/* initGeetest 1.0.0
 * 用于加载id对应的验证码库,并支持宕机模式
 * 暴露 initGeetest 进行验证码的初始化
 * 一般不需要用户进行修改
 */
(function (global, factory) {
  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);
  }
  // eslint-disable-next-line func-names
})(typeof window !== 'undefined' ? window : this, function (window) {
  if (typeof window === 'undefined') {
    throw new Error('Geetest requires browser environment');
  }
  const { document } = window;
  const { Math } = window;
  const head = document.getElementsByTagName('head')[0];

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

  _Object.prototype = {
    _each(process) {
      const { _obj } = this;
      for (const k in _obj) {
        // eslint-disable-next-line no-prototype-builtins
        if (_obj.hasOwnProperty(k)) {
          process(k, _obj[k]);
        }
      }
      return this;
    },
  };
  function Config(config) {
    const 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() {
      const 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(obj) {
      const self = this;
      new _Object(obj)._each(function (key, value) {
        self[key] = value;
      });
    },
  };
  const isNumber = function (value) {
    return typeof value === 'number';
  };
  const isString = function (value) {
    return typeof value === 'string';
  };
  const isBoolean = function (value) {
    return typeof value === 'boolean';
  };
  const isObject = function (value) {
    return typeof value === 'object' && value !== null;
  };
  const isFunction = function (value) {
    return typeof value === 'function';
  };
  const callbacks = {};
  const status = {};
  // eslint-disable-next-line func-names
  const random = function () {
    return parseInt(Math.random() * 10000) + new Date().valueOf();
  };
  // eslint-disable-next-line func-names
  const loadScript = function (url, cb) {
    const script = document.createElement('script');
    script.charset = 'UTF-8';
    script.async = true;
    script.onerror = function () {
      cb(true);
    };
    let loaded = false;
    // eslint-disable-next-line func-names
    // eslint-disable-next-line no-multi-assign
    script.onload = script.onreadystatechange = function () {
      if (
        !loaded &&
        (!script.readyState || script.readyState === 'loaded' || script.readyState === 'complete')
      ) {
        loaded = true;
        // eslint-disable-next-line func-names
        setTimeout(function () {
          cb(false);
        }, 0);
      }
    };
    script.src = url;
    head.appendChild(script);
  };
  const normalizeDomain = function (domain) {
    return domain.replace(/^https?:\/\/|\/$/g, '');
  };
  // eslint-disable-next-line func-names
  const normalizePath = function (path) {
    // eslint-disable-next-line no-param-reassign
    path = path.replace(/\/+/g, '/');
    if (path.indexOf('/') !== 0) {
      // eslint-disable-next-line no-param-reassign
      path = `/${path}`;
    }
    return path;
  };
  const normalizeQuery = function (query) {
    if (!query) {
      return '';
    }
    let 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(/&$/, '');
  };
  // eslint-disable-next-line func-names
  const makeURL = function (protocol, domain, path, query) {
    // eslint-disable-next-line no-param-reassign
    domain = normalizeDomain(domain);

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

    return url;
  };
  const load = function (protocol, domains, path, query, cb) {
    // eslint-disable-next-line func-names
    const tryRequest = function (at) {
      const url = makeURL(protocol, domains[at], path, query);
      // eslint-disable-next-line func-names
      loadScript(url, function (err) {
        if (err) {
          if (at >= domains.length - 1) {
            cb(true);
          } else {
            tryRequest(at + 1);
          }
        } else {
          cb(false);
        }
      });
    };
    tryRequest(0);
  };
  // eslint-disable-next-line func-names
  const 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;
    }
    const cb = `geetest_${random()}`;
    // eslint-disable-next-line no-param-reassign
    // eslint-disable-next-line func-names
    // eslint-disable-next-line no-param-reassign
    window[cb] = function (data) {
      if (data.status === 'success') {
        callback(data.data);
      } else if (!data.status) {
        callback(data);
      } else {
        callback(config._get_fallback_config());
      }
      // eslint-disable-next-line no-param-reassign
      window[cb] = undefined;
      try {
        // eslint-disable-next-line no-param-reassign
        delete window[cb];
        // eslint-disable-next-line no-empty
      } catch (e) {}
    };
    load(
      config.protocol,
      domains,
      path,
      {
        gt: config.gt,
        callback: cb,
      },
      // eslint-disable-next-line func-names
      function (err) {
        if (err) {
          callback(config._get_fallback_config());
        }
      }
    );
  };
  const throwError = function (errorType, config) {
    const errors = {
      networkError: '网络错误',
    };
    if (typeof config.onError === 'function') {
      config.onError(errors[errorType]);
    } else {
      throw new Error(errors[errorType]);
    }
  };
  const detect = function () {
    return !!window.Geetest;
  };
  if (detect()) {
    status.slide = 'loaded';
  }
  const initGeetest = function (userConfig, callback) {
    const 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) {
      const { type } = newConfig;
      const init = function () {
        config._extend(newConfig);
        callback(new window.Geetest(config));
      };
      callbacks[type] = callbacks[type] || [];
      const 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';
              const cbs = callbacks[type];
              for (let i = 0, len = cbs.length; i < len; i += 1) {
                const 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);
      }
    });
  };
  // eslint-disable-next-line no-param-reassign
  window.initGeetest = initGeetest;
  return initGeetest;
});

2、封装initGeeTestWithAjax 、initGeeTestWithAxios通用utils

export function initGeeTestWithAjax(url, captchaKeyName, otherConfigParams, initGeeTestCallback) {
  $.ajax({
    url: `${API_HOST}${url}`, // 加随机数防止缓存
    type: 'get',
    dataType: 'json',
    success: (data) => {
      $(captchaKeyName).val(data.captchaKey); // 设置验证码key
      // 调用 initGeetest 初始化参数
      // 参数1:配置参数
      // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它调用相应的接口
      initGeetest(
        {
          gt: data.geeTestId,
          challenge: data.challenge,
          new_captcha: true, // 用于宕机时表示是新验证码的宕机
          offline: !data.success, // 表示用户后台检测极验服务器是否宕机,一般不需要关注
          product: 'float', // 产品形式,包括:float,popup
          width: '100%',
          ...otherConfigParams,
        },
        (captchaObj) => {
          initGeeTestCallback(captchaObj, data.captchaKey);
        }
      );
    },
  });
}
export function initGeeTestWithAxios(url, captchaKeyName, otherConfigParams, initGeeTestCallback) {
  axios({
    url: `${API_HOST}${url}`, // 加随机数防止缓存
    method: 'GET',
  }).then((data) => {
    if (data) {
      $(captchaKeyName).val(data.captchaKey); // 设置验证码key
      // 调用 initGeetest 初始化参数
      // 参数1:配置参数
      // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它调用相应的接口
      initGeetest(
        {
          gt: data.geeTestId,
          challenge: data.challenge,
          new_captcha: true, // 用于宕机时表示是新验证码的宕机
          offline: !data.success, // 表示用户后台检测极验服务器是否宕机,一般不需要关注
          product: 'popup', // 产品形式,包括:float,popup
          width: '100%',
          ...otherConfigParams,
        },
        (captchaObj) => {
          initGeeTestCallback(captchaObj, data.captchaKey);
        }
      );
    }
  });
}

3、实际应用

const GtDemo = () => {
  /**
   * 其他配置参数: 更多配置参数请参考文档: https://docs.geetest.com/sensebot/apirefer/api/web#onSuccess-callback
   */
  const otherConfigParams = {
    lang: 'en', // 设置验证界面文字的语言 ;类型: 字符串  默认值: zh-cn
    product: 'float', // 设置下一步验证的展现形式 类型: 字符串 默认: popup
    width: '100px', // 设置按钮的长度 类型: 字符串  默认值: 300px
  };

  React.useEffect(() => {
    initGeeTestWithAxios(
      '/iam-16304/public/register',
      '#captchaKey',
      otherConfigParams,
      initGeeTestCallback
    );
  }, []);

  const initGeeTestCallback = (captchaObj, captchaKey) => {
    $('#submit').click((e) => {
      const result = captchaObj.getValidate();
      if (!result) {
        $('#notice').show();
        setTimeout(() => {
          $('#notice').hide();
        }, 2000);
        e.preventDefault();
      }
    });
    captchaObj.appendTo('#captcha-box');
    captchaObj.onReady(() => {
      $('#wait').hide();
    });
    /**
     * 校验成功后进行二次校验
     */
    captchaObj.onSuccess(() => {
      const result = captchaObj.getValidate();
      if (result) {
        $.ajax({
          url: `${API_HOST}/iam-16304/public/validate`, // 加随机数防止缓存
          type: 'POST',
          dataType: 'json',
          data: {
            geetest_challenge: result.geetest_challenge,
            geetest_validate: result.geetest_validate,
            geetest_seccode: result.geetest_seccode,
            captchaKey,
          },
          success: (data) => {
            // 根据服务端二次验证的结果进行跳转等操作
            alert('验证成功,后续为客制化操作');
            console.log('验证成功', data);
          },
        });
      }
    });
  };

  return (
    <>
      <Header title="极验校验" />
      <Content>
        <div style={{ width: 300 }}>
          <h1 style={{ textAlign: 'center' }}>极验验证Demo</h1>
          <Form style={{ width: 300 }} labelAlign="left">
            <TextField label="用户名" />
            <Password label="密码" />
          </Form>
          <div style={{ width: 300 }}>
            <input type="hidden" id="captchaKey" />
            <br />
            <div id="captcha-box">
              <p id="wait">正在加载验证码......</p>
            </div>
            <br />
            <p id="notice" style={{ display: 'none' }}>
              请先完成验证
            </p>
            <Button style={{ float: 'right' }} color="primary" id="submit">
              提交
            </Button>
          </div>
        </div>
      </Content>
    </>
  );
};

export default GtDemo;