Cloudflare Turnstile 验证系统集成指南
概述
Cloudflare Turnstile 是一个现代化的验证解决方案,可以替代传统的验证码系统。本文将介绍如何在 Vue.js 项目中集成 Turnstile 验证系统,包括有感验证和无感验证两种模式。
核心架构设计
1. 配置管理
首先定义验证场景的配置映射:
// Cloudflare Turnstile Site Key 配置映射
export const SITE_KEY_MAP = {
// 本地开发环境
LOCAL: {
mode: "managed",
sitekey: "1x00000000000000000000AA", // 测试用 Site Key
},
// 登录场景 - 有感校验
LOGIN: {
mode: "managed",
sitekey: "0x4AAAAAAA***********", // 生产环境 Site Key
},
// 获取验证码场景 - 无感校验
CODE: {
mode: "invisible",
sitekey: "0x4AAAAAAA***********", // 无感验证 Site Key
},
};
2. 验证场景枚举
定义各种业务场景的验证类型:
export enum CF_TURNSTILE_TYPE {
/** 无验证 */
NONE = "",
/** 登录验证 */
LOGIN= "LOGIN",
/** 获取验证码 */
CODE= "CODE",
// ... 更多业务场景
}
3. 核心管理类
实现 Turnstile 验证的核心管理逻辑:
export class CloudflareMgr {
private static _instance: CloudflareMgr;
private isScriptLoaded = false;
private loadingPromise: Promise<boolean> | null = null;
static get instance(): CloudflareMgr {
if (!CloudflareMgr._instance) {
CloudflareMgr._instance = new CloudflareMgr();
}
return CloudflareMgr._instance;
}
/**
* 初始化 Turnstile 脚本
*/
private async initTurnstileScript(): Promise<boolean> {
if (this.isScriptLoaded) return true;
if (this.loadingPromise) return this.loadingPromise;
this.loadingPromise = new Promise((resolve) => {
if (window.turnstile) {
this.isScriptLoaded = true;
resolve(true);
return;
}
//这部分引用也可以在index.html内直接加入
// <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
const script = document.createElement("script");
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
script.async = true;
script.defer = true;
script.onload = () => {
this.isScriptLoaded = true;
resolve(true);
};
script.onerror = () => {
this.loadingPromise = null;
resolve(false);
};
document.head.appendChild(script);
});
return this.loadingPromise;
}
/**
* 获取验证配置
*/
public getSiteKey(cfType?: CF_TURNSTILE_TYPE): { siteKey: string; mode: string } {
if (cfType) {
const sceneKey = cfType as string;
// 根据场景类型选择配置
let targetConfig: { mode: string; sitekey: string };
if (sceneKey === "CODE") {
targetConfig = SITE_KEY_MAP.CODE;
} else {
targetConfig = SITE_KEY_MAP.LOGIN;
}
if (targetConfig) {
return {
siteKey: targetConfig.sitekey,
mode: targetConfig.mode,
};
}
}
// 默认返回本地开发配置
return {
siteKey: SITE_KEY_MAP.LOCAL.sitekey,
mode: SITE_KEY_MAP.LOCAL.mode,
};
}
/**
* 渲染 Turnstile 验证组件
*/
public async renderTurnstile(
containerId: string,
cfType: CF_TURNSTILE_TYPE,
callback: VerifyCallback,
config?: TurnstileConfig
): Promise<string | null> {
try {
// 确保脚本已加载
const scriptLoaded = await this.initTurnstileScript();
if (!scriptLoaded) {
throw new Error("Failed to load Turnstile script");
}
// 获取容器元素
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container with ID '${containerId}' not found`);
}
container.innerHTML = "";
// 获取验证配置
const siteKeyConfig = config?.siteKey
? { siteKey: config.siteKey, mode: "managed" }
: this.getSiteKey(cfType);
// 构建 Turnstile 选项
const turnstileOptions = {
sitekey: siteKeyConfig.siteKey,
theme: config?.theme || "light",
size: config?.size || "normal",
appearance: config?.appearance || "always",
callback: (token: string) => {
const result: TurnstileResult = {
success: true,
token,
cfType,
};
callback(result);
},
"error-callback": (error: string) => {
const result: TurnstileResult = {
success: false,
cfType,
error: error,
};
callback(result);
},
};
// 渲染组件
if (!window.turnstile) {
throw new Error("Turnstile is not loaded");
}
const widgetId = window.turnstile.render(container, turnstileOptions);
return widgetId;
} catch (error) {
callback(false);
return null;
}
}
}
Vue 组件实现
验证弹窗组件
<template>
<van-dialog
v-model:show="visible"
:close-on-click-overlay="false"
:show-cancel-button="false"
:show-confirm-button="false"
class="cloudflare-verify-dialog"
>
<div class="dialog-content">
<div :id="containerId" class="turnstile-container" :class="{ loading: isLoading }">
<div v-if="isLoading" class="loading-spinner">
<van-loading type="spinner" size="24px" />
</div>
</div>
</div>
</van-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import { CloudflareMgr, CF_TURNSTILE_TYPE, type TurnstileResult } from "@/utils/CloudflareMgr";
interface Props {
modelValue: boolean;
cfType: CF_TURNSTILE_TYPE;
siteKey?: string;
theme?: "light" | "dark" | "auto";
size?: "normal" | "compact";
appearance?: "always" | "execute" | "interaction-only";
}
const props = withDefaults(defineProps<Props>(), {
theme: "light",
size: "normal",
appearance: "always",
});
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
(e: "success", result: TurnstileResult): void;
(e: "error", error: string): void;
}>();
const visible = ref(props.modelValue);
const isLoading = ref(false);
const widgetId = ref<string | null>(null);
// 生成唯一容器ID
const containerId = computed(
() => `turnstile-container-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
);
// 初始化验证
const initVerification = async () => {
if (isLoading.value) return;
isLoading.value = true;
try {
await new Promise((resolve) => setTimeout(resolve, 100));
const mgr = CloudflareMgr.instance;
const config = {
theme: props.theme,
size: props.size,
appearance: props.appearance,
...(props.siteKey && { siteKey: props.siteKey }),
};
widgetId.value = await mgr.renderTurnstile(
containerId.value,
props.cfType,
handleVerificationResult,
config
);
isLoading.value = false;
} catch (error) {
isLoading.value = false;
emit("error", error instanceof Error ? error.message : "Verification failed");
}
};
const handleVerificationResult = (result: TurnstileResult | false) => {
if (result === false) {
emit("error", "Verification failed");
return;
}
if (result.success) {
emit("success", result);
} else {
emit("error", result.error || "Verification failed");
}
};
watch(() => props.modelValue, (val) => {
visible.value = val;
if (val && !widgetId.value) {
initVerification();
}
});
</script>
API 封装
函数式调用接口
import { createApp, h, ref } from "vue";
import { CF_TURNSTILE_TYPE, type TurnstileResult } from "./CloudflareMgr";
import CloudflareVerifyDialog from "@/components/CloudflareVerifyDialog/index.vue";
export interface VerifyOptions {
cfType: CF_TURNSTILE_TYPE;
title?: string;
description?: string;
siteKey?: string;
theme?: "light" | "dark" | "auto";
size?: "normal" | "compact";
appearance?: "always" | "execute" | "interaction-only";
}
export interface VerifyResult {
success: boolean;
token?: string;
cfType?: string;
error?: string;
cancelled?: boolean;
}
/**
* 显示验证弹窗
*/
export function showCloudflareVerify(options: VerifyOptions): Promise<VerifyResult> {
return new Promise((resolve) => {
const container = document.createElement("div");
document.body.appendChild(container);
const visible = ref(true);
const cleanup = () => {
setTimeout(() => {
if (container && container.parentNode) {
app.unmount();
container.parentNode.removeChild(container);
}
}, 300);
};
const handleSuccess = (result: TurnstileResult) => {
visible.value = false;
cleanup();
resolve({
success: true,
...result,
});
};
const handleError = (error: string) => {
visible.value = false;
cleanup();
resolve({
success: false,
cfType: options.cfType,
error: error,
});
};
const app = createApp({
render() {
return h(CloudflareVerifyDialog, {
modelValue: visible.value,
"onUpdate:modelValue": (value: boolean) => { visible.value = value; },
cfType: options.cfType,
siteKey: options.siteKey,
theme: options.theme,
size: options.size,
appearance: options.appearance,
onSuccess: handleSuccess,
onError: handleError,
});
},
});
app.mount(container);
});
}
// 快捷验证方法
export function verifyLogin(): Promise<VerifyResult> {
return showCloudflareVerify({
cfType: CF_TURNSTILE_TYPE.LOGIN,
title: "登录验证",
description: "请完成安全验证以继续登录",
});
}
/**
* 无感验证
*/
export function executeSeamlessVerification(options: VerifyOptions): Promise<VerifyResult> {
return new Promise((resolve) => {
// 创建隐藏容器
const container = document.createElement("div");
container.style.position = "fixed";
container.style.left = "-1000px";
container.style.opacity = "0";
document.body.appendChild(container);
const visible = ref(true);
const cleanup = () => {
setTimeout(() => {
if (container && container.parentNode) {
app.unmount();
container.parentNode.removeChild(container);
}
}, 100);
};
const seamlessOptions = {
...options,
appearance: "execute" as const,
size: "invisible" as const,
};
const app = createApp({
render() {
return h(CloudflareVerifyDialog, {
modelValue: visible.value,
"onUpdate:modelValue": (value: boolean) => { visible.value = value; },
cfType: seamlessOptions.cfType,
siteKey: seamlessOptions.siteKey,
theme: seamlessOptions.theme,
size: seamlessOptions.size,
appearance: seamlessOptions.appearance,
onSuccess: (result: TurnstileResult) => {
cleanup();
resolve({
success: true,
...result,
});
},
onError: (error: string) => {
cleanup();
resolve({
success: false,
cfType: options.cfType,
error: error,
});
},
});
},
});
app.mount(container);
// 30秒超时处理
setTimeout(() => {
if (!container.parentNode) return;
cleanup();
resolve({
success: false,
cfType: options.cfType,
error: "Verification timeout",
});
}, 30000);
});
}
使用示例
基本使用
<script setup lang="ts">
import { showCloudflareVerify, verifyLogin, executeSeamlessVerification } from "@/utils/CloudflareVerifyAPI";
import { CF_TURNSTILE_TYPE } from "@/utils/CloudflareMgr";
// 方式1:使用快捷方法
const handleLogin = async () => {
try {
const result = await verifyLogin();
if (result.success) {
console.log("验证成功,result:", result);
// 继续登录逻辑
}
} catch (error) {
console.error("验证失败:", error);
}
};
// 方式2:自定义验证
const handleCustomVerify = async () => {
try {
const result = await showCloudflareVerify({
cfType: CF_TURNSTILE_TYPE.LOGIN,
title: "login验证",
description: "请完成验证",
theme: "light",
size: "normal",
});
if (result.success) {
console.log("login验证成功");
}
} catch (error) {
console.error("login验证失败:", error);
}
};
// 方式3:无感验证
const handleSeamlessVerify = async () => {
try {
const result = await executeSeamlessVerification({
cfType: CF_TURNSTILE_TYPE.CODE,
});
if (result.success) {
console.log("无感验证成功,可以发送验证码");
}
} catch (error) {
console.error("无感验证失败:", error);
}
};
</script>
核心特性
1. 双模式支持
- 有感验证(Managed):用户需要交互完成验证
- 无感验证(Invisible):后台静默完成验证
2. 灵活配置
- 支持多种主题(light/dark/auto)
- 可配置尺寸(normal/compact)
- 自定义外观行为
开发踩坑
- code=200110:检查cloudflare Turnstile面板配置域名是否正确(一级二级域名都必须相同),在有二级域名的前提下,仅配置一级域名是不行的
- code=600010:检查是否清除浏览器的缓存与cookie,并关闭浏览器的开发者工具(关闭虚拟设备也可)
- 本地测试时需要用localhost打开项目,IP地址校验不通过











网友评论