Введение

Данный шаблон поможет вам добавить защиту формы с помощью Google reCaptcha в ваше SPA приложение. Кроме того, используется прокси пакет для одновременного запуска SPA и .NET приложений. Данная статья основана на статье Adding CAPTCHA on form posts with ASP.NET Core. Если вам нужна защита на Razor pages, то используйте ее.

Создание .NET приложения

Я использую Microsoft Visual Studio 2022 в случае использования других версий экраны и код будут немного отличатся.

Создайте проект ASP .NET Core Web API.

Выберите подходящее имя

И настройте опции как на этом экране, впрочем, это мой выбор для упрощения тестового проекта.

Далее удалите лишние файлы, созданные мастером.

Добавьте следующие пакеты:

  • Microsoft.AspNetCore.SpaServices.Extensions

  • Newtonsoft.Json

Файл Program.cs после правок

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using VueRecaptcha.Services;

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

            // Add services to the container.

            builder.Services.AddControllers();
            builder.Services.AddHttpClient<ReCaptcha>(x =>
            {
                x.BaseAddress = new Uri("https://www.google.com/recaptcha/api/siteverify");
            });
            builder.Services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "ClientApp/build";

            });
            builder.Services
                .AddMvc()
                .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
                    options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
                    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());

                });
            var app = builder.Build();

            // Configure the HTTP request pipeline.
            app.UseStaticFiles();
            app.UseSpaStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.MapControllers();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller}/{action=Index}/{id?}");
            });
            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";
                spa.UseReactDevelopmentServer(npmScript: "serve");
            });
            app.Run();
        }
    }
}

Файл ReCaptcha.cs

using Newtonsoft.Json.Linq;

namespace VueRecaptcha.Services;

public class ReCaptcha
{
    private readonly HttpClient _captchaClient;
    private readonly ILogger<ReCaptcha> _logger;
    private readonly IConfiguration _configuration;
    public ReCaptcha(HttpClient captchaClient, ILogger<ReCaptcha> logger, IConfiguration config)
    {
        _captchaClient = captchaClient;
        _logger = logger;
        _configuration = config;
    }

    public string GetClientMarkup()
    {
        var siteKey = _configuration.GetValue<string>("SiteKey");
        return siteKey;
    }
    public async Task<bool> IsValid(string captcha)
    {
        try
        {
            var secretKey = _configuration.GetValue<string>("SecretKey");
            var postTask = await _captchaClient
                .PostAsync($"?secret={secretKey}&response={captcha}", new StringContent(""));
            var result = await postTask.Content.ReadAsStringAsync();
            var resultObject = JObject.Parse(result);
            dynamic success = resultObject["success"];
            return (bool)success;
        }
        catch (Exception e)
        {
            _logger.LogError("Failed to validate",e);
            return false;
        }
    }
}

Файл UploadModel.cs

namespace VueRecaptcha.ViewModels;

public class UploadModel
{
   public string Email { get; set; } = "";
   public string Name { get; set; } = "";
   public string CaptchaResponse { get; set; } = "";
   public List<IFormFile> Files { get; set; } = new List<IFormFile>();
}

Создание SPA приложения

В папке .NET приложения выполните команду

vue create clientapp

укажите что используете vue 3

перейдите в папку clientapp

добавьте поддержку typescript

vue add typescript

Установите пакеты
npm i -S axios

настройте проект
vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  devServer: {    
    onAfterSetupMiddleware() { // Output the same message as the react dev server to get the Spa middleware working with vue.
      console.info("Starting the development server...");
    }
  },
  transpileDependencies: true
})

HelloWorld.vue

<template>
  <div class="hello">
    <div v-if="uploaded">Файлы загружены</div>
    <form class="uploadForm" v-else>
      <label for="email"
        >Email
        <input type="email" id="email" name="email" required v-model="email" />
      </label>
      <label for="name"
        >Name
        <input type="text" id="name" name="name" required v-model="name" />
      </label>
      <input type="file" multiple hidden id="upload_files" />
      <div class="g-recaptcha" :data-sitekey="siteKey"></div>
      <button @click="addFiles()">Add files</button>
      <button type="submit" @click.prevent="submit">Submit</button>
    </form>
  </div>
</template>
<script lang="ts">
import { Vue } from "vue-class-component";
import axios from "axios";

export default class HelloWorld extends Vue {  
  siteKey: string = "";
  email: string = "";
  name: string = "";
  uploaded: boolean = false;
  async submit() {    
    var formData = new FormData();
    // добавляем в форму поля
    formData.append("email", this.email);
    formData.append("name", this.name);
    // получаем значение капчи
    const resp = (
      document.getElementsByName(
        "g-recaptcha-response"
      )[0] as HTMLTextAreaElement
    ).value;
    formData.append("captchaResponse", resp);
    // поле загрузки файлов
    var imagefile = document.getElementById("upload_files") as HTMLInputElement;
    if (imagefile == null) return;
    if (imagefile.files == null) return;
    const files = imagefile.files;
    for (let i = 0; i < files.length; i++) {
      // добавляем в форму выбранные файлы
      formData.append("files", files[i]);
    }
    var result = await axios.post("/api/upload", formData);
    if (result) {
      console.log(result);
      this.uploaded = true;
    }
    return false;
  }
  addFiles() {
    const upload = document.getElementById("upload_files");
    upload?.click();    
  }
  async mounted() {
    // я сделал так что бы все настройки хранились на сервере, если не нравится лишний запрос то захардкодьте это значение
    var captcha = await axios.get("/api/upload");
    this.siteKey = captcha.data.toString();
    this.createRecaptcha();
  }
  createRecaptcha() {
    var s = document.createElement("script");
    s.setAttribute("src", "https://www.google.com/recaptcha/api.js");
    s.async = true;
    s.defer = true;
    document.body.appendChild(s);
  }
}
</script>

<style scoped>
a {
  color: #42b983;
}
.uploadForm {
  margin: 20px auto;
  width: 300px;
  display: block;
}
.uploadForm label {
  display: block;
  width: 100%;
  margin-bottom: 10px;
}
</style>

Получение своих ключей

Войдите в свой аккаунт reCAPTCHA
https://www.google.com/recaptcha/about/

Добавьте новый сайт, для целей локального тестирования не забудьте добавить localhost

Выберите reCAPTCHA v2 Site Key и Secret Key, скопируйте в файл appsettings.json

И можно запускать отладку приложения в Visual Studio.

Заключение

В итоге мы научились загружать несколько файлов из SPA приложения на бэкенд с проверкой капчей.

Успехов вам и меньше спэма!

Исходный код

Исходный код

Комментарии (3)


  1. nronnie
    27.06.2022 13:57

    Пошаговое How-To без каких-либо пояснений, и уже тем более своих собственных мыслей. Какой смысл публиковать тут такое?


    1. JordanCpp
      27.06.2022 15:43
      -1

      Реакция автора, но ведь я сделяль:)


  1. DrPass
    27.06.2022 15:56
    +1

    @автор, скажу по секрету, из этого вообще можно три статьи сделать…