仅当用户是人类(reCaptcha v3)时,如何加载和插入一些 HTML?

Posted

技术标签:

【中文标题】仅当用户是人类(reCaptcha v3)时,如何加载和插入一些 HTML?【英文标题】:How to load and insert some HTML only if the user is human (reCaptcha v3)? 【发布时间】:2021-12-10 23:45:51 【问题描述】:

目标

为了保护网络应用免受恶意垃圾邮件机器人爬虫和类似恶意行为者的攻击,我的目标是使用 reCAPTCHA v3 来分析访问该网站的用户,如果 Captcha v3 分数足够好(比如 0.5 或更高),请使用 Fetch用于 POST 令牌并对其进行验证的 API,如果分数足够好,如前所述,则在某些 html 中返回电子邮件地址。为简单起见,function loadContactbubble() 在您单击按钮时被执行。

问题

我不确定在哪里实施if (response.score => 0.5) 检查。 前端半工作在这方面,在网络浏览器调试工具中它会给出响应,但在控制台中它将响应打印为undefined 我的实施是否足够安全?密钥不能以某种方式被抽走或类似的吗? 我在浏览器中收到很多 CSP 警告,这可能是生产中的问题吗?

代码

我一直在关注这个指南:https://dev.to/spencer741/google-recaptcha-v3-server-side-validation-using-asp-net-core-5-0-3hfb(这意味着代码与本文的 80-90% 相似)

我的 appsettings.json 包含密钥和 siteverify 链接(API 链接)。

GHttpModels.cs:

    using System;
    using System.Runtime.Serialization;

    namespace _projectname.Tooling
    

        public class GRequestModel
        
            public string path  get; set; 
            public string secret  get; set; 
            public string response  get; set; 
            public string remoteip  get; set; 

            public GRequestModel(string res, string remip)
            
                response = res;
                remoteip = remip;
                secret = Startup.Configuration["GoogleRecaptchaV3:Secret"];
                path = Startup.Configuration["GoogleRecaptchaV3:ApiUrl"];
                if (String.IsNullOrWhiteSpace(secret) || String.IsNullOrWhiteSpace(path))
                
                    //Invoke logger
                    throw new Exception("Invalid 'Secret' or 'Path' properties in appsettings.json. Parent: GoogleRecaptchaV3.");
                
            
        

        //Google's response property naming is 
        //embarrassingly inconsistent, that's why we have to 
        //use DataContract and DataMember attributes,
        //so we can bind the class from properties that have 
        //naming where a C# variable by that name would be
        //against the language specifications... (i.e., '-').
        [DataContract]
        public class GResponseModel
        
            [DataMember]
            public bool success  get; set; 
            [DataMember]
            public string challenge_ts  get; set; 
            [DataMember]
            public string hostname  get; set; 

            //Could create a child object for 
            //error-codes
            [DataMember(Name = "error-codes")]
            public string[] error_codes  get; set; 
        
    

GoogleReCaptchaV3Service.cs:

    using System;
    using System.Threading.Tasks;
    using System.Text.Json;
    using System.Web;
    using System.Net.Http;
    using System.IO;
    using System.Text;
    using System.Runtime.Serialization.Json;

    namespace _projectname.Tooling
    
        public class CaptchaRequestException : Exception
        
            public CaptchaRequestException()
            
            
            public CaptchaRequestException(string message)
                : base(message)
            
            
            public CaptchaRequestException(string message, Exception inner)
                : base(message, inner)
            
            
        

        public interface IGoogleRecaptchaV3Service
        
            HttpClient _httpClient  get; set; 
            GRequestModel Request  get; set; 
            GResponseModel Response  get; set; 
            void InitializeRequest(GRequestModel request);
            Task<bool> Execute();
        

        public class GoogleRecaptchaV3Service : IGoogleRecaptchaV3Service
        
            public HttpClient _httpClient  get; set; 

            public GRequestModel Request  get; set; 

            public GResponseModel Response  get; set; 

            public HttpRequestException HttpReqException  get; set; 

            public Exception GeneralException  get; set; 

            public GoogleRecaptchaV3Service(HttpClient httpClient)
            
                _httpClient = httpClient;
            

            public void InitializeRequest(GRequestModel request)
            
                Request = request;
            

            public async Task<bool> Execute()
            
                // Notes on error handling:
                // Google will pass back a 200 Status Ok response if no network or server errors occur.
                // If there are errors in on the "business" level, they will be coded in an array;
                // CaptchaRequestException is for these types of errors.

                // CaptchaRequestException and multiple catches are used to help seperate the concerns of 
                //  a) an HttpRequest 400+ status code 
                //  b) an error at the "business" level 
                //  c) an unpredicted error that can only be handled generically.

                // It might be worthwhile to implement a "user error message" property in this class so the
                // calling procedure can decide what, if anything besides a server error, to return to the 
                // client and any client handling from there on.
                try
                

                    //Don't to forget to invoke any loggers in the logic below.

                    //formulate request
                    string builtURL = Request.path + '?' + HttpUtility.UrlPathEncode($"secret=Request.secret&response=Request.response&remoteip=Request.remoteip");
                    StringContent content = new StringContent(builtURL);

                    Console.WriteLine($"Sent Request builtURL");

                    //send request, await.
                    HttpResponseMessage response = await _httpClient.PostAsync(builtURL, null);
                    response.EnsureSuccessStatusCode();

                    //read response
                    byte[] res = await response.Content.ReadAsByteArrayAsync();

                    string logres = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"Retrieved Response: logres");

                    //Serialize into GReponse type
                    using (MemoryStream ms = new MemoryStream(res))
                    
                        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(GResponseModel));
                        Response = (GResponseModel)serializer.ReadObject(ms);
                    

                    //check if business success
                    if (!Response.success)
                    
                        throw new CaptchaRequestException();
                    

                    //return bool.
                    return true; //response.IsSuccessStatusCode; <- don't need this. EnsureSuccessStatusCode is now in play.
                            
                catch (HttpRequestException hre)
                
                    //handle http error code.
                    HttpReqException = hre;

                    //invoke logger accordingly

                    //only returning bool. It is ultimately up to the calling procedure
                    //to decide what data it wants from the Service.
                    return false;
                
                catch (CaptchaRequestException ex)
                

                    //Business-level error... values are accessible in error-codes array.
                    //this catch block mainly serves for logging purposes. 

                    /*  Here are the possible "business" level codes:
                        missing-input-secret    The secret parameter is missing.
                        invalid-input-secret    The secret parameter is invalid or malformed.
                        missing-input-response  The response parameter is missing.
                        invalid-input-response  The response parameter is invalid or malformed.
                        bad-request             The request is invalid or malformed.
                        timeout-or-duplicate    The response is no longer valid: either is too old or has been used previously.
                    */

                    //invoke logger accordingly 

                    //only returning bool. It is ultimately up to the calling procedure 
                    //to decide what data it wants from the Service.
                    return false;
                
                catch (Exception ex)
                
                    // Generic unpredictable error
                    GeneralException = ex;

                    // invoke logger accordingly

                    //only returning bool. It is ultimately up to the calling procedure 
                    //to decide what data it wants from the Service.
                    return false;
                
            
        
    

Startup.cs:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
//from captchav3
using _projectname.Tooling;

namespace _projectname

    public class CookieCheckMiddleware
    
        private readonly RequestDelegate _next;

        public CookieCheckMiddleware(RequestDelegate next)
        
            _next = next;
        
        public async Task Invoke(HttpContext httpContext)
        
            if (httpContext.Request.Cookies["ModalShown"] == null && httpContext.Request.Path != "/Cookies")
            
                httpContext.Response.Redirect("/Cookies?q="+ httpContext.Request.Path);
            
            await _next(httpContext); // calling next middleware

        
    

    // Extension method used to add the middleware to the HTTP request pipeline.
    public static class CookieCheckMiddlewareExtensions
    
        public static IApplicationBuilder UseCookieCheckMiddleware(this IApplicationBuilder builder)
        
            return builder.UseMiddleware<CookieCheckMiddleware>();
        
    

    public class Startup
    
        internal static IConfiguration Configuration  get; private set; 
        public Startup(IConfiguration configuration)
        
            Configuration = configuration;
        

        //public IConfiguration Configuration  get; 

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        
            // Captcha v3
            services.AddHttpClient<IGoogleRecaptchaV3Service, GoogleRecaptchaV3Service>();
            services.AddTransient<IGoogleRecaptchaV3Service, GoogleRecaptchaV3Service>();
            services.AddControllers();
            //Register dependencies
            services.AddRazorPages();
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
        

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        
            if (env.IsDevelopment())
            
                app.UseDeveloperExceptionPage();
            
            else
            
                //app.Use(async (ctx, next) =>
                //
                //    await next();

                //    if (ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
                //    
                //        //Re-execute the request so the user gets the error page
                //        string originalPath = ctx.Request.Path.Value;
                //        ctx.Items["originalPath"] = originalPath;
                //        ctx.Request.Path = "/Cloud";
                //        await next();
                //    
                //);
                // orig
                //app.UseExceptionHandler("/Errors/0");
                app.UseStatusCodePagesWithRedirects("/Errors/0");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            

            app.UseHttpsRedirection();
            app.UseStaticFiles();
    

            app.UseRouting();
            // TEST
            app.UseCookieCheckMiddleware();

            app.UseAuthentication();
            app.UseAuthorization();

            var cookiePolicyOptions = new CookiePolicyOptions
            
                MinimumSameSitePolicy = SameSiteMode.Strict,
            ;
            app.UseCookiePolicy(cookiePolicyOptions);

            app.UseEndpoints(endpoints =>
            
                endpoints.MapRazorPages();
                // Experimental
                endpoints.MapControllers();
            );
        
    


_Layout.cshtml:

<button onclick="loadContactbubble();">Load contacts</button>

site.js(为简洁起见仅提供函数):

function loadContactbubble() 
    grecaptcha.execute('sitekeyhere',  action: 'onclick' ).then(function (token) 
        console.log(token);
        fetch("/load/contactbubble?RecaptchaToken=" + token, 
            method: "POST",
            body: token,
        )
    ).then((response) => 
        console.log(response);
        if (!response.ok) 
            const errorBuild = 
                type: "Error",
                message: response.message || "Something went wrong",
                data: response.data || "",
                code: response.code || "",
            ;
        
    
        //addText("Error: " + JSON.stringify(errorBuild));
        //toggleLoader(2, "hidden");
        //return;
    )

ApiController.cs:

using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using _projectname.Tooling;
using System.Threading.Tasks;

namespace _projectname.Controllers

    public class SignUpModel
    
        public string RecaptchaToken  get; set; 
    

    [ApiController]
    [Route("load/contactbubble")]
    public class SignUp : ControllerBase
    
        IGoogleRecaptchaV3Service _gService  get; set; 
        public SignUp(IGoogleRecaptchaV3Service gService)
        
            _gService = gService;
        

        [HttpPost]
        public async Task<IActionResult> Post([FromQuery] SignUpModel SignUpData)
        

            GRequestModel rm = new GRequestModel(SignUpData.RecaptchaToken,
                                                 HttpContext.Connection.RemoteIpAddress.ToString());

            _gService.InitializeRequest(rm);

            if (!await _gService.Execute())
            
                //return error codes string.
                return Ok(_gService.Response.error_codes);
            

            //call Business layer

            //return result
            return base.Content("<div>Welcome human! Here is our secret e-mail: test@test.com</div>", "text/html");

        
    


错误

如果我点击按钮,控制台会打印出以下内容:

Uncaught (in promise) TypeError: can't access property "ok", response is undefined

响应在网络选项卡中包含正确的 HTML,这很奇怪。

我该如何解决这个问题?

【问题讨论】:

【参考方案1】:

你的功能

function (token) 
    console.log(token);
    fetch("/load/contactbubble?RecaptchaToken=" + token, 
        method: "POST",
        body: token,
    );

不返回任何内容,因此传递给下一个.then((response) =&gt; ... 的参数是未定义

让这个函数返回获取的数据,它应该可以工作:

function (token) 
    console.log(token);

    return fetch("/load/contactbubble?RecaptchaToken=" + token, 
        method: "POST",
        body: token
    );

(好吧,它至少应该将获取结果转发到下一个.then((response) =&gt; ...。我没有在你的代码中寻找其他错误,所以“它应该可以工作”是了解我在这里解释的一个问题...)

【讨论】:

欢迎来到社区 :-) 似乎添加 return 以某种奇怪的格式返回响应,这意味着 .then((response) =&gt;... 由于某种原因不起作用:-( 没关系,response.ok 工作正常,现在如何附加 HTML 内容?我只看到四种响应类型,并且文本不是我想要的,以便将 HTML 附加到页面中..

以上是关于仅当用户是人类(reCaptcha v3)时,如何加载和插入一些 HTML?的主要内容,如果未能解决你的问题,请参考以下文章

密钥如何与 reCAPTCHA v3 Enterprise 配合使用?

如何解决 Google v3 reCaptcha 超时?

Google 的 reCAPTCHA v3 的工作原理

如何知道 reCAPTCHA v3 是不是有效?

谷歌 reCAPTCHA v3 左下角

如何在服务器端验证 Google reCAPTCHA v3?