Skip to content

使用示例

DEMO 的源码如下,可以作为接入的参考

前端

基于 Next.js

组件代码

tsx
import React, { useState, useEffect, useRef } from "react";
import Script from "next/script";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";

import { apis } from "../../../api";
import useMessage from "../../../components/hook-message";

import styles from "./index.module.css";

export default function Demo() {
  const [captchaId, setCaptchaId] = useState("");
  const captcha = useRef<InstanceType<(typeof window)["BumoCaptcha"]>>();
  const { showMessage } = useMessage();

  const captchaVerify = async () => {
    if (!captchaId) {
      showMessage("请先完成滑动验证", "warning");
      return;
    }
    try {
      const result = await apis.sendMessageCode({
        captchaId: captchaId,
        phone: "[手机号]",
      });
      if (result.isPass) {
        showMessage("验证成功,验证码将会发送", "success");
      } else {
        showMessage("验证失败", "error");
      }
    } catch (e) {}
    captcha.current?.reset();
    setCaptchaId("");
  };

  const onCaptchaLoaded = () => {
    captcha.current = new window.BumoCaptcha({
      containerId: "BUMO_CAPTCHA",
      id: "[产品 ID]",
      successCallback: function (token: string) {
        setCaptchaId(token);
      },
    });
  };

  return (
    <>
      <Script
        onLoad={onCaptchaLoaded}
        src={`https://cdn2.bumo.ink/apps/captcha-sdk/captcha.js?t=${Math.floor(
          Date.now() / 60000
        )}`}
      ></Script>
      <div className={styles.wrap}>
        <div>
          <div className={styles["form-item"]}>
            <TextField
              fullWidth
              id="phone"
              label="手机号 演示无需填写"
              variant="standard"
            />
          </div>
          <div className={styles["form-item"]}>
            <div id="BUMO_CAPTCHA"></div>
          </div>
          <div className={styles["form-item"]}>
            <TextField
              id="code"
              label="验证码 演示无需填写"
              variant="standard"
            />
            <Button
              onClick={captchaVerify}
              size="small"
              sx={{ verticalAlign: "bottom" }}
              variant="outlined"
            >
              {"获取验证码"}
            </Button>
          </div>
          <div>
            <Button
              variant="contained"
              fullWidth
              onClick={() => {
                showMessage("请点击[获取验证码]体验服务端验证", "warning");
              }}
            >
              登录
            </Button>
          </div>
        </div>
      </div>
      ;
    </>
  );
}

类型定义

typescript
// types.d.ts
interface OptionsType {
  containerId: string; // 容器HTML元素ID
  id: string; // 申请的产品 ID
  timeout?: number; // 接口请求超时时间,默认 10s
  placeHolder?: string; // 默认 placeholder
  succussPlaceHolder?: string; // 成功后的 placeholder
  loadingPlaceHolder?: string; // 判断重的 placeholder
  retryPlaceHolder?: string; // 重试的 placeholder
  headlessPlaceHolder?: string; // 检查为 headless 时 placeholder
  successCallback?: (captchaId: string) => void; // 滑块检测通过后的回调
  failCallback?: () => void; // 滑块检测未通过后的回调
}
interface BumoCaptchaResult {
  reset(options?: { placeHold?: string }): void;
}

interface BumoCaptchaConstructor {
  new (options: OptionsType): BumoCaptchaResult;
}

interface Window {
  BumoCaptcha: BumoCaptchaConstructor;
}

后端

基于 Nodejs(Koa) 实现,前端调用接口 sendMessageCode 的主要实现如下:

typescript
import fetch from "node-fetch";

export const captchaVerify = async (options: { captchaId: string }) => {
  const { captchaId } = options;
  const controller = new AbortController();

  // 设置超时时间 10s
  setTimeout(() => controller.abort(), 10000);

  try {
    const response = await fetch(
      `https://captcha.bumo.tech/api/captcha/server/get-result`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          referer: "https://bumo.tech",
          authorization: "Bearer [API-KEY]",
        },
        body: JSON.stringify({ r: captchaId }),
      }
    );
    if (response.status === 200 || response.status === 400) {
      // 服务端正常返回
      const data = (await response.json()) as {
        code: number;
        data: {
          p: boolean; // 检测是否通过
        };
        message: string;
        success: boolean;
      };
      if (data.success) {
        return {
          isPass: data.data.p,
        };
      }
      if (data.code === 20001) {
        // 超过请求速率限制,为避免影响业务,建议通过
        return {
          isPass: true,
        };
      }
    }
    if (response.status === 401 || response.status === 403) {
      // 无权限,一般是authorization key过期,或者错误
      return {
        isPass: false,
        status: 401,
      };
    }
    throw response;
  } catch (e) {
    // 前端滑动验证时,服务端故障,纯前端生成的 ID,若此时调用后端服务也出现服务器故障,则可认为滑动验证码服务端故障
    // 此时为避免导致业务不可用,应该判断通过
    const IS_NO_SERVER_ID = captchaId.startsWith("NOSERVER_");
    if (IS_NO_SERVER_ID) {
      return {
        isPass: true,
      };
    }

    // 其他错误
    return {
      isPass: false,
    };
  }
};

const sendMessageCode = async (options: {
  phone: string;
  captchaId: string;
}) => {
  const checkResult = await captchaVerify({ captchaId });

  if (checkResult.isPass) throw new Error("滑动验证未通过");

  // 发送验证码逻辑
  ...
};