GEETEST 行为认证与 SMS 短信登录(下)

上期主要分享了验证码的一些基本概念,以及前端如何接入 GeeTest SDK。本期将从业务流程的角度和大家讲解从点击发送验证码到验证通过后的 60 S 倒计时内,都经历了哪些流程。旨在提供一套企业级常用的 GeeTest 接入 SMS 短信业务的解决方案。

1. 流程图

先来看下完整的业务流程

接下来我们对照着图逐步实现流程。

2. 业务流程

这里涉及到登录表单逻辑、按钮逻辑、GeeTest 拉起逻辑,它们有如下包含关系:

我们的思路是将其拆分,分别封装到对应的 hooks 中,这样既能将业务相互解耦,也能做到有状态逻辑的复用。

💡请注意,本文中的代码示例是对具体业务代码的抽象,简化了部分复杂结构的数据,移除了一些 TS 类型,让代码看上去更加简洁易懂,大家切记求其意而忘其形。

2.1 提取 useGeeTest hook

useGeeTest hooks 相对简单,主要包含极验 GeeTest 组件的渲染及其配置,包括以下数据和方法:

属性 类型 说明
geeParams data 极验渲染参数(gt、challenge、new_captcha、offline)
setGeeParams method 保存 geeParams 参数
renderGeeTest method 极验组件渲染函数
geeTestCheck method 极验校验通过后,将组件返回的参数传给后端,进行二次校验

钩子结构如下:

ts 复制代码
// src/hooks/useGeeTest.ts
const useGeeTest = () => {
  const geeParams = reactive({});

  const setGeeParams = (params) => {
    Object.assign(geeParams, params);
  };
  const renderGeeTest = () => {};
  const geeTestCheck = () => {};

  return {
    renderGeeTest,
    setGeeParams,
    geeTestCheck
  }
}

export default useGeeTest;

geeParams: 由后端返回,也就是说 GeeTest 校验的配置不全是由前端决定的,其中的验证 id、流水号、服务器是否宕机等参数,都需要后端从极验服务器获取。

renderGeeTest:渲染 GeeTest 组件。注意,它接收一个叫 handleSuccess 异步回调,当校验通过后,组件会往该回调中注入结果,我们就可以通过该回调向后端发起二次校验请求。

ts 复制代码
const renderGeeTest = (handleSuccess) => {
    document.querySelector("#captchaBox").innerHTML = "";

    if (window.initGeetest) {
      window.initGeetest(
        // 配置
        {
          width: "100%",
          product: "float",
          ...geeParams,
        },

        // 回调,captchaObj 为验证实例
        captchaObj => {
          document.querySelector("#captchaBox").innerHTML = "";
          captchaObj.appendTo("#captchaBox");
          captchaObj
            .onReady(function () {
              // 监听验证按钮的 DOM 生成完毕事件;
            })
            .onSuccess(async () => {
              // 监听验证成功事件
              const {
                geetest_challenge: challenge,
                geetest_seccode: seccode,
                geetest_validate: validate
              } = captchaObj.getValidate();

              if (handleSuccess) {
                await handleSuccess({ challenge, validate, seccode })
              };
            })
            .onError(error => {
              // 监听验证出错事件
              console.log(`${error.error_code} ${error.msg}`);
            });
        }
      );
    }
};

geeTestCheck: 就是我们二次请求校验的方法,我们随后要将该方法当做 renderGeeTest 的异步回调传进去。

ts 复制代码
const geeTestCheck = (flowId, captchaInfo) => {
    try {
        const data = await captchaCheck({ // captchaCheck 请求伪代码
            flowId,
            captchaInfo: JSON.stringify(captchaInfo),
        })
        // todo: 处理 data
    } catch (err) {
        // todo: 处理 error
    }
};

这里对返回的结果还没处理,因为这个结果和发送短信的请求返回的结果相似,我们稍后再回来补全。

2.2 提取 useSms hook

useSms hook 是我们的重头戏,主要负责按钮点击相关逻辑,包括发送短信请求、按钮禁用与倒计时、是否拉起 GeeTest 校验等。

其中包含以下主要数据和方法:

属性 类型 说明
flowId data 流程 id
disabled data 按钮是否禁用
timer data 倒计时计时器
seconds data 秒(倒计时)
sendSms method 发送短信
countdown method 倒计时
start method 实现发送验证码的主函数(校验、发送、倒计时、拉起极验等)
end method 解除禁用,移除计时器

钩子的结构大致如下:

ts 复制代码
// src/hooks/useSms
const useSms = () => {
  const flowId = ref('');
  const disabled = ref(false);
  const timer = ref(null);
  const seconds = ref('');

  const start = () => {};
  const sendSms = () => {};
  const countdown = () => {};
  const end = () => {};

  return {
    disabled,
    timer,
    seconds
  }
};

export default useSms;

我们先实现独立的功能,最后在将各个功能整合到 start() 中去。

2.2.1 倒计时

倒计时逻辑相对独立,我们先实现这个小 utils。

ts 复制代码
const end = () => {
  seconds.value = "";
  disabled.value = false;
  clearInterval(timer.value);
};

const countdown = (time = 60) => {
  seconds.value = `${time}`;
  disabled.value = true;
  timer.value = setInterval(() => {
    if (time > 0) {
      time -= 1;
      seconds.value = `${time}`;
    } else {
      end();
    }
  }, 1000);
};

countdown 用于开启禁用,并开始 60 s 倒计时。

2.2.2 流程初始化与短信发送

sendSms() 短信发送包含两个步骤:

  1. 业务流程初始化;
  2. 发送短信。

在初始化时,后端会派发一个 flowId。因为一次完整的登录会涉及到多个接口的调用,它们都是隶属于同一个流程的不同阶段。而同一时间,会有大量的用户在进行登录。所以,请求时带上当前的 flowId,后端才知道当前的请求是属于哪个用户的哪个阶段。

ts 复制代码
const sendSms = async (phone) => {
  try {
    // step 1: 获取 flowId
    const { flowId } = await loginInit(); // loginInit 是初始化请求伪代码
    flowId.value = data.flowId;

    // step 2: 发送短信
    const data = await smsSend({ // smsSend 是短信请求伪代码
      flowId,
      phone: `+86-${phone}`,
    });
    return data;
  } catch (error) {
    flowId.value = "";
    return Promise.reject(error);
  }
}

2.2.3 nextAction 处理步骤

短信发送不单单是发送短信了事,它有两种结果:

  1. 无需 GeeTest 校验,直接发送短信,进行登录;
  2. 需要先调用 GeeTest 校验。

两种结果会放在诸如 nextAction 的字段中。比如 nextAction = 'login',表示第一种情况,直接发送短信。nextAction = 'geetest',则表示需要调用 GeeTest 进行用户行为校验。

如果是第二种情况,后端还会返回 GeeTest 所需的配置参数,以供前端拉起 GeeTest。

两种结果会导致不同的处理方式,如果是第二种情况,那么会先通过 GeeTest 组件校验后,将校验结果传给后端,后端在进行二次校验接口后,才会发送短信。

包括刚刚在 useGeeTest 中提到的 geeTestCheck(),二次校验返回的结果也有一个 nextAction,因为 GeeTest 二次校验可能成功,也可能失败。如果失败,我们也需要重新校验或者提醒他稍后重试。

所以,前端接下来要做的,就需要根据 nextAction 做出不同的处理。我们会将这部分处理放到 start() 中实现。现在,我们只需要关注发送请求,并将结果返回即可。

2.3 整合 start 流程

我们在 start 中将所有流程进行整合,开始吧!

先在 useSms 中引入 useGeeTest

ts 复制代码
const useSms = () => {
    const { setGeeParams, renderGeeTest, geeTestCheck } = useGeeTest();
    // ......
}

2.3.1 表单校验

在点击按钮后,会先校验表单中填写的手机格式是否正确,所以 start() 中需要接收一个 Form 实例,并调用其校验方法(我用的组件库是 Element-plus),

ts 复制代码
const useSms = (formEl) => {
    // 略......
    formEl.validateField('phone', async (isValid) => {
        if (!isValid) return;
    })
}

2.3.2 开启按钮禁用与倒计时

按钮禁用与倒计时的执行时机很重要。正常来讲,应该等发送短信成功后,再开始倒计时,不然发送不成功,也要等 60 秒,这显然不合理。但是,这其中潜藏了一个非常隐蔽的 Bug。

在弱网环境下,初始化和发送短信的请求会变得很慢,在等待的时间内,按钮是不会禁用的,此时用户仍然可以连续点击按钮,那禁用与倒计时的防刷机制就将毫无意义。

也许你会说,我可以加个防抖优化一下啊,但是请注意,防抖只能限制点击的次数,无法控制请求的时间。试想一个极端的场景:比如我给了 800 ms 的防抖,但请求可能需要 1 s,用户恰好在 800 ms 到 1 s 的间隔内就可以继续点击了,而他又恰好以这种间隔时间连续点击,依旧能跳过防刷机制。

所以,我们给出的解决方案是:在第一次点击时就禁用按钮并开启倒计时。如果请求成功,那么继续流程,如果请求中某个接口报错,那么立即解除禁用。

这种思路相当于将请求的等待时间放在倒计时的 60 s 之内完成。因为成功的话,本来就是需要禁用的,而失败再去解禁就可以了。

js 复制代码
const useSms = (formEl, phone) => {
    // 略......
    formEl.validateField('phone', async (isValid) => {
        if (!isValid) return;

        // 开启禁用和倒计时
        clearInterval(timer.value);
        disabled.value = true;
        countdown();

        try {
            // todo: 短信流程
        } catch(error) {
            // 流程中有任何错误,都立即解除禁用
            end();
        }
    })
}

我们利用 try...catch 去捕获流程中的错误,只要有任何错误,就立即解除禁用。

2.3.3 处理请求结果

接下来,我们把短信流程放入 try 块中:

ts 复制代码
// 略......
try {
    const data = await sendSms();
    if (data.nextAction === 'geetest') {
        // 需先极验,通过再发短信
        setGeeParams(data.geeParams);
        renderGeeTest(captchaInfo => {
            geeTestCheck(flowId.value, captchaInfo)
                .then(() => {
                    ElMessage.success('短信验证码已发送');
                })
                .catch(() => {
                    flowId.value = '';
                    ElMessage.error('验证失败,请稍后重新验证。');
                });
        });
    } else {
        // 无需极验,直接发送短信
        ElMessage.success('短信验证码已发送');
    }
}
// 略......

这个结构很清晰,我们根据短信请求的结果,进行对应处理。其中,当后端要求下一步是 geetest 时,我们拉起极验,并将成功的回调传入。

现在我们回头继续完成 geeTestCheck() 中的结果处理部分,和上述处理类似:

ts 复制代码
const geeTestCheck = (flowId, captchaInfo) => {
    try {
        const data = await captchaCheck(/*...*/); // captchaCheck 请求伪代码
        if (data.nextAction === 'geetest') {
            return Promise.reject(data);
        }
    } catch (err) {
        return Promise.reject(err);
    }
};

其中失败有两种,一种是接口失败,另一种是接口成功,但后端返回的 nextAction === 'geetest' 依旧表明需要进一步极验校验。如果是第二种,我们可以继续调用 renderGeeTest() 拉起极验,这里我们简化一下,统一当做错误处理,并提示用户稍后重试即可。

2.4 登录页

最后,我们在页面中使用 hooks。

js 复制代码
<script setup lang="ts">
const { seconds, disabled, start, end } = useSms;

onBeforeUnmount(() => {
  end();
});
</script>

<template>
  <el-button
    link
    :type="disabled ? 'info' : 'primary'"
    :disabled="disabled"
    @click="start(formRef, form.phone)"
  >
    {{ seconds.length > 0 ? seconds + "S" : "获取验证码" }}
  </el-button>
</template>

至此,大功告成。

总结

本篇文章偏向于业务解决方案,和大家分享了如何在我们的登录流程中接入三方行为验证和短信发送,希望对大家有帮助哈!😀

往期相关文章

GEETEST 行为认证与 SMS 短信登录(上)

相关推荐
千穹凌帝几秒前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵11 分钟前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc00722 分钟前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥31 分钟前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG32 分钟前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英1 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel
Java开发追求者1 小时前
在CSS中换行word-break: break-word和 word-break: break-all区别
前端·css·word
好名字08211 小时前
monorepo基础搭建教程(从0到1 pnpm+monorepo+vue)
前端·javascript
pink大呲花1 小时前
css鼠标常用样式
前端·css·计算机外设
Flying_Fish_roe1 小时前
浏览器的内存回收机制&监控内存泄漏
java·前端·ecmascript·es6