jQuery + SpringMVC 集成极验验证码插件

极验有一款行为验证的插件,其实就是个验证码插件,包括滑块和点选的验证方式,这里记录一下如何接入基于 jQuery + SpringMVC 的 Web 端项目

更多精彩

写在前面的话

  1. 接入这个行为验证是因为自己网站的注册验证码短信接口被人攻击了,所以就打算在发送注册验证码之前加一个验证
  2. 网上搜索一番看到极验的这个插件,发现还不少公司用的,像经常去的 B 站,和原来偶尔会逛得数字尾巴都有用
  3. 这个插件本身效果也还蛮不错的,所以就选择接入
  4. 插件本身提供免费试用,只是有几个限制,免费版只提供滑块验证,以及每小时最高 500 次的交互量,咱们这小网站感觉够用了
  5. 不过昨天晚上市场那边正好在做一个推广活动,突然给我打电话说用户反馈注册时候无法通过行为验证
  6. 我上去一看,行为验证报错了,问了下他们多少人在注册,那边说就 200 个人,我想的是这应该也达不到 500 次的峰值
  7. 这得一个人验证错 3 次,才能达到 600 次,所以我就体所应当的以为是后端跪了,但其实没有
  8. 后来去极验的后端一看,竟然交互量达到了 636 次,超过了峰值,所以接口返回为空了,汗颜
  9. 既然市场这么给力,那我今天就联系一下极验的客服问下升级套餐呗,结果问完之后我就开始着手下线这个功能了
  10. 他们的价格差不多是这样的,也不复杂,默认套餐价格区间是 6-20w 一年 ,6w 是支持每小时 1w 次的峰值交互量
  11. 我说 1w 都多了,我目前只需要 2000 次,那边给了一个特惠套餐,3w 一年,每小时 5000 次的峰值交互量
  12. 我顺便问了一下,这个 3w 一年只仅仅这个插件的授权使用,还是他们几个产品都能用(他们貌似还有其他几个产品,虽然并不感兴趣)
  13. 那边说只是验证码,这个我还真的比较汗颜,觉得他们这相当于一个接口卖 3w 一年了,不过其实应该是我酸,科科
  14. 总之这里只是记录一下我的接入流程,因为接下来我准备删掉这个功能换成别的实现方式了
  15. 我现在还在想昨天 200 个人的注册真的可以达到 600+ 次的交互量吗,在有人引导注册的前提下,该不会是 …. ?

相关网址

  1. 行为验证-Captcha-验证码-Captcha-极验行为验证
  2. geetest-java
  3. geetest-WEB-front

后端对接实现

下载并接入集成包

  1. 在前面提供的 geetest-java 中可以下载到插件后端环境的集成包,也可以通过 git clone https://github.com/GeeTeam/gt3-java-sdk.git 直接下载
  2. 下载下来其实是 Java 语言的本地文档、DEMO 、SDK 、以及和后端语言配对的前端依赖 JS
    • 我觉得这里后端如果能提供一个在线的 Maven 或 Gradle 依赖地址其实更好
  3. /src/sdk/GeetestLib.javasrc/demo/demo1/GeetestConfig.java 引入到自己项目即可
  4. 打开 GeetestConfig.java ,将其中的 geetest_idgeetest_key 修改成自己的即可
    • 这两个参数需要到他们 极验后台登录 获取,登录后选择行为验证
    • 之后还需要新增验证,填写基本信息后即可拿到这两个值,如下图
    • 左下角的 ID 对应 geetest_id ,KEY 对应 geetest_key
      image

编写验证码服务

  1. 后端验证的逻辑分为两步验证,第一步初始化,其实就是相当于生成验证码,第二步才是用户发起验证请求
  2. 文档中分别使用了 doGet 演示初始化,doPost 演示接收验证请求
  3. 实际项目中当然不会使用 Servlet 来做这些事情,这里使用的是 SpringMVC
  4. 两个操作可以放在同一个 Service 中,例如 CaptchaServiceImpl.java

封装请求参数

  1. 单独封装起来是因为两步验证都会用到
  2. 请求参数其实就是每个用户的唯一标识,文档中使用的是 用户 ID终端类型 以及 IP 地址
  3. 我这里因为是未登录状态下的验证,所以拿不到用户 ID ,所以直接使用的 IP 地址
private HashMap<String, String> getParams() {
    String clientIp = URIUtil.getClientIp(request);

    HashMap<String, String> params = Maps.newHashMap();
    params.put("user_id", clientIp);
    params.put("client_type", "web");
    params.put("ip_address", clientIp);

    return params;
}

生成验证码,对应第一步初始化

  1. getParams() 获取参数就是上一个函数
public String generateCaptcha() {
    // 初始化极验服务
    GeetestLib lib = new GeetestLib(GeetestConfig.getGeetest_id(), GeetestConfig.getGeetest_key(), GeetestConfig.isnewfailback());

    // 验证预处理
    int status = lib.preProcess(getParams());

    // 将服务状态存放到 Session 中,在第二步验证时会用到
    request.getSession().setAttribute(lib.gtServerStatusSessionKey, status);

    // 返回生成字串
    return lib.getResponseStr();
}

接收用户验证请求,并返回存储处理结果

  1. request.getParameter() 可以直接使用,是因为我的 Service 集成了一个通用的父类,在里面直接注入了 HttpServletRequest
  2. 通过 request 获取的参数都不是自定义的,由极验前端的 gt.js 内部提供
  3. saveMessage() 是验证成功后将当前参与验证的手机号和结果存储到 Redis 中,时效 5 分钟
    • 通过行为验证,正式发起验证码请求时,会先通过请求的手机号去 Redis 中获取行为验证结果
    • 存在且成功才会发验证码,否则会提示进行行为验证
  4. TSharkException() 是自定义的异常处理,由 Controller 捕获后抛到前端弹出提示框告知用户
public void checkCaptcha(String mobile) {
    // 初始化极验服务
    GeetestLib lib = new GeetestLib(GeetestConfig.getGeetest_id(), GeetestConfig.getGeetest_key(), GeetestConfig.isnewfailback());

    // 接收前端参数,由前端 JS 内部封装处理
    String challenge = request.getParameter(GeetestLib.fn_geetest_challenge);
    String validate = request.getParameter(GeetestLib.fn_geetest_validate);
    String seccode = request.getParameter(GeetestLib.fn_geetest_seccode);

    // 取出第一步初始化验证时存储的服务状态
    int status = (Integer) request.getSession().getAttribute(lib.gtServerStatusSessionKey);

    int result = 0;

    if (status == 1) {
        // 服务器在线
        result = lib.enhencedValidateRequest(challenge, validate, seccode, getParams());
    } else {
        // 服务器离线
        result = lib.failbackValidateRequest(challenge, validate, seccode);
    }

    if (result == 1) {
        // 保存验证信息
        saveMessage(mobile, new MessageBean(mobile, result));
    } else {
        throw new TSharkException("行为验证失败,请检查使用环境");
    }
}

控制层定义请求

  1. 因为是传统项目(非前后分离),所以使用的注解依旧是以下方式
    • 请求相同,分别使用 RequestMethod.GETRequestMethod.POST 来区分第一步验证和第二步验证
  2. SimpleActionHandler 是自定义封装的 Service 异常处理类
    • request 在继承的 AbstractBaseController 自定义父类中统一声明
  3. ResponseData 是自定义封装的返回结果类
  4. 后端的服务就完成了,实现起来还是非常简洁的,点个赞
@Controller
@RequestMapping("/captcha")
public class CaptchaController extends AbstractBaseController {

    @Autowired
    private CaptchaServiceImpl captchaService;

    @RequestMapping(value = "", method = RequestMethod.GET)
    @ResponseBody
    public ResponseData init() {
        return new SimpleActionHandler(request) {
            @Override
            protected void doHandle(ResponseData responseData) throws Exception {
                responseData.setData(captchaService.generateCaptcha());
            }
        }.handle();
    }

    @RequestMapping(value = "", method = RequestMethod.POST)
    @ResponseBody
    public ResponseData check(@RequestParam final String mobile) {
        return new SimpleActionHandler(request) {
            @Override
            protected void doHandle(ResponseData responseData) throws Exception {
                captchaService.checkCaptcha(mobile);
            }
        }.handle();
    }

}

前端对接实现

前端依赖 JS 在哪里

  1. 前端依赖的 JS 在 geetest-WEB-front 接口文档中是找不到的,因为不同服务端语言对应的 JS 不一样,所以将其放置在了后端集成包中
  2. 具体位置如下图所示


    image
  3. 官方也给了相关提示,如下图


    image

引入前端依赖 JS

  1. 将 JS 加入到项目并引入即可
  2. 因为我的项目里用到了 jQuery ,所以以下代码中先引入了
<script type="text/javascript" src="${ctx }/assets/plugins/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="${ctx }/assets/plugins/geetest/gt.js"></script>

准备一个 HTML 区域用于显示行为验证组件

  1. 具体样式因人而异,总之就是提供一个 div 用于放置初始化好的验证组件即可
<section>
  <label class="label">行为验证 <span class="text-danger pull-right"></span></label>
  <label class="input captcha">
    <div class="captcha-tip">行为验证加载中</div>
  </label>
</section>

初始化行为验证插件

  1. 插件的初始化函数是 initGeetest() ,来自上文中的 gt.js
  2. 但其实在初始化之前需要先发送一个异步请求,去调用服务端的初始化接口,因为初始化函数中的值,是由服务端初始化接口返回的
  3. 初始化成功后在回调函数中通过 captchaObj.appendTo() 将行为验证组件加入指定的页面容器中
  4. 因为组件初始化需要先访问后端接口,考虑到网络延迟,所以一般会先给个提示,比如显示一句话 “行为验证加载中”
    • 如果要在初始化结束后隐藏,在 captchaObj.onReady() 中调用即可,这些官方文档都有介绍
// 发起第一步验证
$.ajax({
  url: '${ctx}/captcha',
  type: 'GET',
  dataType: 'json',
  success: function (data) {
    var result = JSON.parse(data.data)

    // 初始化行为验证
    initGeetest({
      gt: result.gt,
      challenge: result.challenge,
      new_captcha: result.new_captcha,
      offline: !result.success,
      product: 'float',
      width: '100%'
    }, function (captchaObj) {
      // 显示行为验证操作元素
      captchaObj.appendTo($memberRegisterPanel.find('.captcha'))

      captchaObj.onReady(function () {
        getCurrentPanel().find('.captcha-tip').hide()
      })

        // .. 执行后续操作
    })
  }
})

用户前端验证成功后,发起后端核验请求

  1. 这一步的操作会在用户进行滑块验证并成功后被调用
  2. 如果滑块都没有弄对,则不会发起后端核验
  3. 这里发起的验证实际上就是去调用后端的第二步验证接口
    • mobile 是传入的手机号
    • 其他的三个参数都是极验插件内部返回的验证结果,用于发送到后端进行二次核验
  4. 在请求的回调中可以判断核验结果,并执行后续操作
captchaObj.onSuccess(function () {
  var result = captchaObj.getValidate()
  
    // 发起之前可以先进行一些自定义验证,比如手机号是否填写,格式是否错误
    // 如果自定义验证没有通过,可以使用 captchaObj.reset() 重置验证组件状态

  // 发起二次验证
  $.ajax({
    url: '${ctx}/captcha',
    type: 'POST',
    data: {
      mobile: mobileVal,
      geetest_challenge: result.geetest_challenge,
      geetest_validate: result.geetest_validate,
      geetest_seccode: result.geetest_seccode
    },
    dataType: 'json',
    success: function (data) {
      if (!data.success) {
        // 验证成功后的具体操作
      }
    }
  })
})

一句话总结

  1. 极验的这个行为验证组件接入的方式比较简单,实现的效果也蛮不错的,但我还是认为就一个验证码功能一年收费 6w 起,太贵了,搞不起搞不起,科科
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容