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



目标

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

问题

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

代码

我遵循以下指南:https://dev.to/spencer741/google-recaptcha-v3-server-side-validation-using-asp-net-core-5-0-3hfb(这意味着代码与本文中的80-90%相似)

我的appsettings.json包含密钥和站点验证链接(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;
)
}

ApiControlle.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,这很奇怪。

我该如何解决这个问题?

您的函数

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

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

让这个函数返回提取的数据,它应该有望工作:

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

(好吧,它至少应该将提取结果转发到下一个.then((response) => ...。我没有在你的代码中寻找其他错误,所以">它应该有望工作"是关于我在这里解释的一个问题的理解…)

相关内容

  • 没有找到相关文章

最新更新