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

1,213 阅读7分钟

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

1. 流程图

先来看下完整的业务流程

GeeTest 验证流程.png

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

2. 业务流程

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

Snipaste_2023-08-29_17-37-06.png

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

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

2.1 提取 useGeeTest hook

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

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

钩子结构如下:

// 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 异步回调,当校验通过后,组件会往该回调中注入结果,我们就可以通过该回调向后端发起二次校验请求。

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 的异步回调传进去。

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 校验等。

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

属性类型说明
flowIddata流程 id
disableddata按钮是否禁用
timerdata倒计时计时器
secondsdata秒(倒计时)
sendSmsmethod发送短信
countdownmethod倒计时
startmethod实现发送验证码的主函数(校验、发送、倒计时、拉起极验等)
endmethod解除禁用,移除计时器

钩子的结构大致如下:

// 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。

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,后端才知道当前的请求是属于哪个用户的哪个阶段。

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

const useSms = () => {
    const { setGeeParams, renderGeeTest, geeTestCheck } = useGeeTest();
    // ......
}

2.3.1 表单校验

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

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

2.3.2 开启按钮禁用与倒计时

按钮禁用与倒计时的执行时机很重要,错误的执行时机可能会导致防刷机制无法生效,它们看似是一个东西,但其实执行时机并不相同。

按钮禁用:点击即禁用,遇到任何错误导致最终发送短信的流程失败立即解禁。这种错误包括但不限于任一请求失败、GeeTest 加载失败、GeeTest 验证失败。最后,在短信发送成功且 60 s 倒计时结束后恢复。

倒计时:触发的唯一条件就是短信发送流程的最后一个请求成功,并在 60 s 倒计时结束后恢复。

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

        // 开启禁用
        clearInterval(timer.value);
        disabled.value = true;

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

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

2.3.3 处理请求结果

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

// 略......
try {
    const data = await sendSms();
    if (data.nextAction === 'geetest') {
        // 需先极验,通过再发短信
        setGeeParams(data.geeParams);
        renderGeeTest(captchaInfo => {
            geeTestCheck(flowId.value, captchaInfo)
                .then(() => {
                    ElMessage.success('短信验证码已发送');

                    // 发送成功,再开始倒计时
                    countdown();
                })
                .catch(() => {
                    flowId.value = '';
                    ElMessage.error('验证失败,请稍后重新验证。');
                    
                    // 失败不要忘了也需解除禁用
                    end();
                });
        });
    } else {
        // 无需极验,直接发送短信
        ElMessage.success('短信验证码已发送');
    }
}
// 略......

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

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

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。

<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 短信登录(上)