使用Geetest极验进行人机识别

人机验证的历史

「人机识别」在互联网安全领域有着重要而广泛的作用,其主要作用就是防止暴力破解,暴破密码需要高频次的试探操作,每次试探密码要控制在100毫秒级以内才有意义,因此黑客都是编写与我们HTML结构类似的脚本,加上他们自己的黑客字典来自动攻击,黑客字典来自于已破系统的数据库,比如大名鼎鼎的 CSDN ,要注意的是虽然这些字典具有相当高的匹配度,但仍需要多次尝试。
引入人机识别后,脚本被感应屏蔽,只有操作者是真正的人才能通过检测,因此可以大大防范暴力破解,同时降低了系统读取数据库的频次,如果做好拦截策略,甚至可以防范一定程度的DDOS攻击。
那么如何判定操作者是人还是机器呢,随着互联网技术的交替更迭,这一技术有着长远的发展历史。从一开始歪歪扭扭的字符和数字的验证码(这一方法目前是最不安全的,殊不知现在到处的云API提供图形识别的功能,一次识别只需要几分钱,有的甚至免费识别);到弹出一张图片,让用户快速选择是小猫还是小狗,或是其他什么;还有图片上是数学计算公式,让用户计算一下结果;有的是播放音频文件,让用户锻炼一下听力,等等。。。终极的就是发送验证码到用户手机上了,比如一些支付系统。
这些技术要么就是实现起来容易,被破解也容易,要么就是实现起来困难或者会产生额外的成本,破解起来也有难度。今天我想给大家介绍一下远程托管式的人机校验服务。
提起第三方的托管人机校验,最厉害的要数Google的 recaptcha,我试用过两天,部署起来确实方便好用,但可惜的是被墙在外面,一般用户几乎加载不了,体验很差。无奈选择了国产的 Geetest 极验服务。

使用极验

官网介绍的极验的验证流程图如下:

  • 用户进入登陆页面时,请求本地服务器授予 Geetest 令牌
  • 服务器在接受这一请求时,使用Geetest提供的多语言开发包,调用授信函数(该函数的参数包括几个注册账号后提供的密钥),这一函数会向极验服务器发送状态请求,获得Geetest认可后构造一个登陆令牌给本地服务器,本地服务器再转发给浏览器。
  • 如果本地服务器与Geetest服务器连接不畅,不能及时获得Geetest的响应,Geetest官方推荐本地采用备用验证框架,比如验证码什么的,不要影响客户登陆。如果你足够信任Geetest的服务器不宕机,或者不遭受DDOS攻击,那么这一步可以免掉。
  • 客户端在接受到Geetest令牌后,使用JS显示极验的拖动UI,有三种模式显示:嵌入式、浮动式、弹出式。
  • 用户在页面上使用鼠标拖动Geetest的图片拖动功能,将拼图拖动到合理区域,完成验证。根据Geetest官网介绍,Geeteset并不仅仅判断用户是否拖动到正确位置,Geetest会同时对用户的拖动轨迹,时间,以及浏览器的Cookie进行检测,利用一些列行为分析的算法最终计算出屏幕面前的是人还是脚本。
  • 当用户的拖动行为得到Geetest的认可时,Geetest会返回一个认证令牌,嵌入到登陆页面的表单中,当用户提交form时,这一串令牌会被到本地服务器接受,并再次通过多语言API进行Geetest服务器的校验。如果认证成功,则放行进行用户层的密码、用户名校验,如果不成功,说明有可能是脚本在操作,需要拦截。

    具体步骤

    了解了上面整个流程后,我们来编码,首先是登陆页面向本地服务器的令牌请求:

    登陆页面主要代码:

    <form action="/login" id="login-form" method="POST" autocomplete="off" onsubmit="return checkSubmit();">
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      <input type="text" name="jblog_username" placeholder="username">
      <input type="password" name="jblog_password" placeholder="password">
      <div id="popup-captcha"></div>
      <button id="btn-submit">Login</button>
    </form>
    <script src="https://static.geetest.com/static/tools/gt.js"></script>
    <script type="text/javascript">
      var captchaObj;
      $(function(){
          $.get("${rc.contextPath}/getGeetestStep1Data", function(result){
              if(result.data.success == 1){
                  initGeetest({
                      gt: result.data.gt,
                      challenge: result.data.challenge,
                      product: "popup",
                  }, handlerFloat);
              }
          },"json");
          var handlerFloat = function (me) {
              // 将验证码加到id为popup-captcha的元素里
              captchaObj = me;
              captchaObj.bindOn("#btn-submit");
              captchaObj.appendTo("#popup-captcha");
          };
      });
      function checkSubmit(){
          if(captchaObj.getValidate()){
              return true;
          }else{
              captchaObj.show();
              return false;
          }
      }
    </script>
    

    SpringMVC中的令牌响应:

    /**
    * Geetest验证码输出 
    */
    @RequestMapping(value = "/getGeetestStep1Data", method = RequestMethod.GET)  
    public ModelAndView getGeetestStep1Data(HttpSession session) {  
      Result result = new Result();
    
      GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);
    
      int gtServerStatus = gtSdk.preProcess();
    
      //将服务器状态设置到session中
      session.setAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY, gtServerStatus);//放入session,用于后面的验证
    
      String resStr = gtSdk.getResponseStr();
    
      result.setData(resStr);
    
      return new ModelAndView("ajaxResult", "result", result);
    }
    

    登陆按钮提交后的Spring Security接受:

    public class GeetestFilter extends UsernamePasswordAuthenticationFilter{
    
      @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
              throws AuthenticationException {
          String username = request.getParameter("jblog_username");
          String password = request.getParameter("jblog_password");
    
          if(BlogUtils.isEmpty(username, password)){
              throw new UsernameNotFoundException("用户名、密码不能为空!");
          }
    
          //进行Geetest极验校验
          GeetestLib gtSdk = new GeetestLib(BlogConfig.GEETEST_AK, BlogConfig.GEETEST_SK);
    
          String challenge = request.getParameter("geetest_challenge");
          String validate  = request.getParameter("geetest_validate");
          String seccode   = request.getParameter("geetest_seccode");
    
          Integer status = (Integer) request.getSession().getAttribute(GeetestLib.SERVER_STATUS_SESSION_KEY);
    
          //根据Geetest的官方说明,如果Geetest服务器宕机,需要切换到本地验证,暂时这边不设本地验证,Geetest宕机直接通过。
          if(status == 1){
              if(BlogUtils.isEmpty(challenge,validate,seccode) || gtSdk.enhencedValidateRequest(challenge, validate, seccode) == 0 ){
                  System.out.println("未通过人机识别");
                  request.getSession().setAttribute("exceptionMsg", "未通过人机识别");
                  throw new UsernameNotFoundException("人机识别失败,请检查您的行为!");
              }
          }
    
          return super.attemptAuthentication(request, response);
      }
    }
    

    关于最后一段使用Spring Security进行Geetest校验的部分,还有待商榷,因为Spring Security实在是太复杂了,以后我会考虑使用Shiro,或者直接用SpringMVC拦截器进行Geetest的校验。
    最后制作一张使用Geetest进行登陆的GIF:

    其他

    与Geetest服务类似还有一些,比如新兴的 「螺丝帽」
    大致的流程都差不多,就看个人喜好使用了。

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区