跳至主要內容

使用 Vue 3 與 .Net 8 實作 Google reCAPTCHA v2

Pamis Wang大约 9 分鐘全端VueVue 3TypeScriptASP.NET CoreASP.NET Core 8C#API

使用 Vue 3 實作 Google reCAPTCHA(v2)

前言

近期因為工作需求和電子商務相關,所以來做個範例。

開發功能之前不免俗要來吐槽一下,
研究顯示,網頁機器人解 CAPTCHA 能力優於人類open in new window
本來 reCaptcha 是要擋機器人的,但最後都是拿來為難人類自己。

開發目標

前端使用 Vue 3.5 搭配後端使用 .Net 8 完成

先決條件

  • Google 帳號:沒有就去辦一個唄
  • Node.js:本次範例用 Node.js v22.11.0
  • ASP.NETopen in new window Core 8:微軟親兒子 Visual Studio 2022 裝下去就有了

註冊服務

先到 https://www.google.com/recaptcha/about/open in new window 畫面

點選 「v3 Admin Console」

接著就會看到這註冊表單畫面

  • 標籤

  • reCAPTCHA 類型
    本次範例選擇 「驗證問題 (v2)」 > 「我不是機器人」核取方塊」,實際上可根據業務需求調整

  • 網域
    因為是在本機上做開發,所以先填入 localhost127.0.0.1

完成之後會產生一組 網站金鑰 與 密鑰

網站金鑰是給前端用的,密鑰是給後端在伺服器端驗證用的。

文件參考

渲染方式

雖然很多教學都直接給程式碼照抄,但建議還是先把文件看懂,
上方畫面中點選「進一步瞭解用戶端整合」可以看官方文件 。

reCAPTCHA v2 官方文件 前端顯示open in new window

本頁說明如何在網頁上顯示和自訂 reCAPTCHA v2 小工具。
如要顯示小工具,可以採取下列任一做法:
自動顯示小工具或
明確算繪小工具
如要瞭解如何自訂小工具,請參閱「設定」一節。舉例來說,您可以指定小工具的語言或主題。
請參閱「驗證使用者的回應」,確認使用者是否已成功回答人機驗證問題。

這個小工具就是平常會看到的核取方塊和那討人厭的九宮格

核取方塊
核取方塊
真的很機車
真的很機車

自動顯示和明確算繪差異在於載入方式
自動顯示當網頁完成載入時自動顯示
明確轉譯需要透過 JavaScript 程式碼手動觸發顯示

原生 HTML 範例

這邊寫一個原生 HTML 來示範手動渲染,
按下按鈕 會把一個以 recaptcha-container 為 ID 的 div 標籤換成核取方塊。

<html>
  <head>
    <title>reCAPTCHA V2 Demo</title>
    <script src="https://www.google.com/recaptcha/api.js" async defer></script>
  </head>

  <body>
    <button id="showCaptcha">顯示驗證碼</button>

    <div id="recaptcha-container"></div>

    <script>
      // 等待 API 完全載入
      window.onload = function () {
        // 取得按鈕元素
        const button = document.getElementById("showCaptcha");

        let isRendered = false;

        // 監聽按鈕點擊事件
        button.addEventListener("click", function () {
          // 確保只渲染一次
          if (!isRendered) {
            try {
              // 使用 grecaptcha.render() 來渲染驗證碼
              grecaptcha.render("recaptcha-container", {
                sitekey: "sitekey", // 替換成你的 site key
                callback: function (response) {
                  // 驗證成功的回調函數
                  console.log("驗證成功:", response);
                },
                "error-callback": function () {
                  // 發生錯誤的回調函數
                  console.error("驗證發生錯誤");
                },
                "expired-callback": function () {
                  // 驗證過期的回調函數
                  console.log("驗證已過期");
                  isRendered = false; // 允許重新渲染
                },
              });

              isRendered = true;
              button.textContent = "驗證碼已顯示";
              button.disabled = true;
            } catch (error) {
              console.error("渲染驗證碼時發生錯誤:", error);
            }
          }
        });
      };
    </script>
  </body>
</html>

建立前端專案

參考官方教學建立一個空專案

​ 創建一個 Vue 應用open in new window

把樣式清掉方便開發

不用套件

參考上方原生 HTML 寫法換成 Vue3 的寫法,
並新建一個 component。

src\components\ReCaptchaV2.vue

<template>
  <div>
    <div ref="re-captcha-v2"></div>
    <p v-if="error" class="error">{{ error }}</p>
    <p v-if="token">Token:</p>
    <p v-if="token">{{ token }}</p>
  </div>
</template>

<script setup lang="ts">
  import { ref, useTemplateRef, onMounted } from "vue";

  // 介面定義 reCAPTCHA 提供的 JavaScript API
  interface ReCaptchaV2 {
    render(container: HTMLDivElement, options: ReCaptchaV2Parameters): number;
    reset(widgetId?: number): void;
    getResponse(widgetId?: number): string;
  }
  // 介面定義 grecaptcha.render 參數
  interface ReCaptchaV2Parameters {
    sitekey: string;
    callback?: (response: string) => void;
    "expired-callback"?: () => void;
    "error-callback"?: () => void;
  }

  // ref 型別定義
  const reCaptchaV2 = useTemplateRef<HTMLDivElement>("re-captcha-v2");
  const error = ref("");
  const token = ref("");

  // 這裡要填入你從 Google reCAPTCHA 管理後台獲得的 site key
  const siteKey = "****************************************";
  let widgetId: number | null = null;

  // 工具函數:取得 grecaptcha 實例
  function getGrecaptcha() {
    return (window as unknown as { grecaptcha: ReCaptchaV2 }).grecaptcha;
  }

  /**
   * 載入 reCAPTCHA script
   */
  async function loadReCaptcha() {
    const script = document.createElement("script");
    script.src = "https://www.google.com/recaptcha/api.js";
    script.async = true;
    script.defer = true;
    document.head.appendChild(script);
  }

  /**
   * 初始化 reCAPTCHA
   */
  async function initialReCaptcha() {
    const captcha = getGrecaptcha();
    if (!captcha) {
      error.value = "reCAPTCHA 尚未載入";
      return;
    }
    try {
      widgetId = captcha.render(reCaptchaV2.value!, {
        sitekey: siteKey,
        callback: (response: string) => {
          console.log(response);
          token.value = response;
        },
        "expired-callback": () => {
          error.value = "reCAPTCHA 回應到期時執行,讓使用者必須重新驗證。";
          console.log("error", error.value);
        },
        "error-callback": () => {
          error.value = "reCAPTCHA 驗證發生錯誤。";
          console.log("error", error.value);
        },
      });
      console.log("widgetId", widgetId);
    } catch {}
  }

  onMounted(async () => {
    try {
      await loadReCaptcha();
      // 確保 grecaptcha 已完全載入
      const captcha = getGrecaptcha();
      if (captcha) {
        initialReCaptcha();
      }
    } catch (e) {
      console.error(e);
      error.value = "reCAPTCHA 載入失敗";
      // emit('error', error.value)
    }
  });
</script>

<style scoped>
  .error {
    color: red;
    font-size: 14px;
    margin-top: 8px;
  }
</style>

使用套件

看到上面落落長的範例,是不是很想用套件了呢?
沒錯這種爛大街的需求肯定會有套件可以爽!

vue3-recaptcha2open in new window

<template>
  <vue-recaptcha
    v-show="showRecaptcha"
    sitekey="****************************************"
    size="normal"
    theme="light"
    hl="tr"
    :loading-timeout="loadingTimeout"
    @verify="recaptchaVerified"
    @expire="recaptchaExpired"
    @fail="recaptchaFailed"
    @error="recaptchaError"
    ref="recaptchaRef"
  >
  </vue-recaptcha>
  <p v-if="token">Token:</p>
  <p v-if="token">{{ token }}</p>
</template>

<script setup lang="ts">
  import { ref } from "vue";
  import vueRecaptcha from "vue3-recaptcha2";

  const showRecaptcha = ref(true);
  const loadingTimeout = ref(30000);
  const token = ref("");

  const recaptchaRef = ref<InstanceType<typeof vueRecaptcha> | null>(null);

  const recaptchaVerified = (response: string) => {
    console.log(response);
    token.value = response;
  };

  const recaptchaExpired = () => {
    recaptchaRef.value?.reset();
  };

  const recaptchaFailed = () => {};

  const recaptchaError = (reason: string) => {
    console.log(reason);
  };
</script>

前端的部分就先到這邊,接著是後端的部分。

建立後端專案

reCAPTCHA v2 官方文件 後端驗證open in new window

用 Visual Studio 開一個 Web API 新專案,
本次範例我以 RecaptchaV3 作為專案名稱。

開發目標是接收前端的 token 後,攜帶後端的密鑰一併發送請求給 Google 來判斷是否成功。

建立必要檔案

畢竟是框架還是得乖乖照規矩走,至於檔案怎麼擺可以自行調整。

appsettings.json 加入這段 存放前面拿到的 網站金鑰 和 密鑰。

  "ReCaptchaSettings":
  {
    "SiteKey": "************************************************",
    "SecretKey": "************************************************"
  },

開一個 Models 資料夾

ReCaptchaSettings.cs 用來讀取 appsettings.json

namespace RecaptchaV3.Models
{
    public class ReCaptchaSettings
    {
        public const string Position = "ReCaptchaSettings";
        public string SiteKey { get; set; } = string.Empty;
        public string SecretKey { get; set; } = string.Empty;
    }
}

ReCaptchaRequest.cs 用來接收前端 token 的請求物件

using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace RecaptchaV3.Models
{
    public class ReCaptchaRequest
    {
        [Required]
        [JsonPropertyName("reCaptchaResponse")]
        public string ReCaptchaResponse { get; set; } = string.Empty;
    }
}

ReCaptchaRequest.cs 用來接收 Google 驗證後的回應物件

using System.Text.Json.Serialization;

namespace RecaptchaV3.Models
{
    public class ReCaptchaResponse
    {
        [JsonPropertyName("success")]
        public bool Success { get; set; }

        [JsonPropertyName("challenge_ts")]
        public string ChallengeTs { get; set; }

        [JsonPropertyName("hostname")]
        public string Hostname { get; set; }

        [JsonPropertyName("error-codes")]
        public string[] ErrorCodes { get; set; }
    }
}

實作發送邏輯

開一個 Services 資料夾

ReCaptchaService.cs 實作發送請求

using Microsoft.Extensions.Options;
using RecaptchaV3.Models;
using System.Text.Json;

namespace RecaptchaV3.Services
{
    public interface IReCaptchaService
    {
        Task<ReCaptchaResponse?> ValidateReCaptchaAsync(string reCaptchaResponse);
    }

    public class ReCaptchaService : IReCaptchaService
    {
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly ReCaptchaSettings _settings;
        private readonly ILogger<ReCaptchaService> _logger;


        public ReCaptchaService(IHttpClientFactory httpClientFactory, IOptionsMonitor<ReCaptchaSettings> settings, ILogger<ReCaptchaService> logger)
        {
            _httpClientFactory = httpClientFactory;
            _settings = settings.CurrentValue;
            _logger = logger;
        }

        public async Task<ReCaptchaResponse?> ValidateReCaptchaAsync(string reCaptchaResponse)
        {
            try
            {
                var parameters = new Dictionary<string, string>
                {
                    {"secret", _settings.SecretKey},
                    {"response", reCaptchaResponse}
                };
                using HttpClient httpClient = _httpClientFactory.CreateClient();
                var content = new FormUrlEncodedContent(parameters);
                var response = await httpClient.PostAsync("https://www.google.com/recaptcha/api/siteverify", content);
                var jsonString = await response.Content.ReadAsStringAsync();
                var recaptchaResponse = JsonSerializer.Deserialize<ReCaptchaResponse>(jsonString);

                return recaptchaResponse;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "reCAPTCHA 驗證過程發生錯誤");
                return null;
            }
        }
    }
}

加入發送端點

using Microsoft.AspNetCore.Mvc;
using RecaptchaV3.Models;
using RecaptchaV3.Services;

namespace RecaptchaV3.Controllers
{
    [ApiController]
    [Route("recaptcha")]
    public class RecaptchaController : ControllerBase
    {
        private readonly IReCaptchaService _reCaptchaService;

        private readonly IHttpClientFactory _httpClientFactory;

        public RecaptchaController(IReCaptchaService reCaptchaService, IHttpClientFactory httpClientFactory)
        {
            _reCaptchaService = reCaptchaService;
            _httpClientFactory = httpClientFactory;
        }

        [HttpPost("verify")]
        public async Task<IActionResult> Post(ReCaptchaRequest request)
        {
            if (string.IsNullOrEmpty(request.ReCaptchaResponse)) { return BadRequest("reCAPTCHA response is required"); }


            var ReCaptchaResponse = await _reCaptchaService.ValidateReCaptchaAsync(request.ReCaptchaResponse);

            if (ReCaptchaResponse == null || !ReCaptchaResponse.Success) { return BadRequest("reCAPTCHA validation failed"); }

            return Ok(new { success = true });
        }

    }
}

依賴注入服務

直接貼出完整的 Program.cs,反白的部分要貼對地方。

幾個被反白的說明如下:

using RecaptchaV3.Models;
using RecaptchaV3.Services;

namespace RecaptchaV3
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // 讀取允許列表
            string[] corsOrigins = builder.Configuration.GetSection("AllowedHosts").Get<string>()!.Split(';');

            // 加入具有預設原則的 CORS 和中介軟體
            builder.Services.AddCors(options =>
            {
                options.AddDefaultPolicy(
                    policy =>
                    {
                        if (corsOrigins.Contains("*"))
                        {
                            policy.SetIsOriginAllowed(_ => true);
                        }
                        else
                        {
                            policy.WithOrigins(corsOrigins);
                        }
                        policy.AllowAnyHeader();
                        policy.AllowAnyMethod();
                        policy.AllowCredentials();
                    });
            });

            // 注入 ReCaptcha 必要設定與服務
            builder.Services.Configure<ReCaptchaSettings>(builder.Configuration.GetSection(ReCaptchaSettings.Position));
            builder.Services.AddScoped<IReCaptchaService, ReCaptchaService>();

            // 注入 IHttpClientFactory
            builder.Services.AddHttpClient();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            // 啟用預設 CORS 原則
            app.UseCors();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}


















 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 
 
 

 
 












 
 









功能串接

後端功能完成後接下來就是把前後端功能串起來,因此要將前面前端的程式做調整。

根據上面不用套件的版本來改,
加入 81-106 行的 verifyToken 實作與後端的 API 溝通,
並把 verifyToken 函式 放入 67 行的 callback 內,
當使用者驗證成功就會自動呼叫後端確認有效性。

src\components\ReCaptchaV2.vue

<template>
  <div>
    <div ref="re-captcha-v2"></div>
    <p v-if="error" class="error">{{ error }}</p>
    <p v-if="token">Token:</p>
    <p v-if="token">{{ token }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'

// 介面定義 reCAPTCHA 提供的 JavaScript API
interface ReCaptchaV2 {
  render(container: HTMLDivElement, options: ReCaptchaV2Parameters): number
  reset(widgetId?: number): void
  getResponse(widgetId?: number): string
}
// 介面定義 grecaptcha.render 參數
interface ReCaptchaV2Parameters {
  sitekey: string
  callback?: (response: string) => void
  'expired-callback'?: () => void
  'error-callback'?: () => void
}

// ref 型別定義
const reCaptchaV2 = useTemplateRef<HTMLDivElement>('re-captcha-v2')
const error = ref('')
const token = ref('')

// 這裡要填入你從 Google reCAPTCHA 管理後台獲得的 site key
const siteKey = '6LdCfo4qAAAAAEhXixXDxRABEuqJgae94Dqyfgbt'
let widgetId: number | null = null

// 工具函數:取得 grecaptcha 實例
function getGrecaptcha() {
  return (window as unknown as { grecaptcha: ReCaptchaV2 }).grecaptcha
}

/**
 * 載入 reCAPTCHA script
 */
async function loadReCaptcha() {
  const script = document.createElement('script')
  script.src = 'https://www.google.com/recaptcha/api.js'
  script.async = true
  script.defer = true
  document.head.appendChild(script)
}

/**
 * 初始化 reCAPTCHA
 */
async function initialReCaptcha() {
  const captcha = getGrecaptcha()
  if (!captcha) {
    error.value = 'reCAPTCHA 尚未載入'
    return
  }
  try {
    widgetId = captcha.render(reCaptchaV2.value!, {
      sitekey: siteKey,
      callback: (response: string) => {
        console.log(response)
        token.value = response
        verifyToken(token.value)
      },
      'expired-callback': () => {
        error.value = 'reCAPTCHA 回應到期時執行,讓使用者必須重新驗證。'
        console.log('error', error.value)
      },
      'error-callback': () => {
        error.value = 'reCAPTCHA 驗證發生錯誤。'
        console.log('error', error.value)
      },
    })
  } catch {}
}

// 發送 token 到後端驗證
async function verifyToken(token: string) {
  try {
    const response = await fetch('https://localhost:7291/recaptcha/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ reCaptchaResponse: token }),
    })

    const result = await response.json()
    console.log(result)
    if (result.success) {
      // 驗證成功,繼續你的邏輯
      console.log('驗證成功')
      alert('驗證成功')
    } else {
      // 驗證失敗
      console.log('驗證失敗')
      alert('驗證失敗')
    }
  } catch (error) {
    console.error('驗證發生錯誤:', error)
  }
}

onMounted(async () => {
  try {
    await loadReCaptcha()
    // 確保 grecaptcha 已完全載入
    const captcha = getGrecaptcha()
    if (captcha) {
      initialReCaptcha()
    }
  } catch (e) {
    console.error(e)
    error.value = 'reCAPTCHA 載入失敗'
    // emit('error', error.value)
  }
})
</script>

<style scoped>
.error {
  color: red;
  font-size: 14px;
  margin-top: 8px;
}
</style>


































































 













 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
























參考資料

上次編輯於:
貢獻者: Pamis Wang