Appearance
使用示例
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]",
'x-id': '[captchaId]'
},
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("滑动验证未通过");
// 发送验证码逻辑
...
};