美文网首页
vue3项目加cloudflare Turnstile校验

vue3项目加cloudflare Turnstile校验

作者: FredericaJ | 来源:发表于2025-08-26 15:41 被阅读0次

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地址校验不通过

参考官方文档
参考其他应用

相关文章

网友评论

      本文标题:vue3项目加cloudflare Turnstile校验

      本文链接:https://www.haomeiwen.com/subject/fuodajtx.html