Blazor recaptcha 验证属性 IHttpContextAccessor 始终为 null

Blazor recaptcha validation attribute IHttpContextAccessor is always null

我想我会尝试使用 Blazor 服务器端,到目前为止,我已经设法以某种方式克服了大多数令人头疼的问题并享受它,直到现在。

我正在尝试为 Google Recaptcha v3 编写一个验证程序,它需要用户的 IP 地址。通常我会通过以下方式获取 IHttpContextAccessor:

var httpContextAccessor = (IHttpContextAccessor)validationContext.GetService(typeof(IHttpContextAccessor));

但那现在returns无效!我还发现尝试以同样的方式获取 IConfiguration 失败了,但为此,我可以在 Startup.cs.

中创建一个静态 属性

这是一天工作的最后一关,搞得我莫名其妙。

关于如何将该 IP 地址放入验证器的任何想法?

谢谢!

编辑:

我刚刚发现使 httpContextAccessor 为 null 的错误!

((System.RuntimeType)validationContext.ObjectType).DeclaringMethodthrew an exception of type 'System.InvalidOperationException'

这是验证器:

public class GoogleReCaptchaValidationAttribute : ValidationAttribute
    {

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        Lazy<ValidationResult> errorResult = new Lazy<ValidationResult>(() => new ValidationResult("Google reCAPTCHA validation failed", new String[] { validationContext.MemberName }));

        if (value == null || String.IsNullOrWhiteSpace(value.ToString()))
        {
            return errorResult.Value;
        }

        var configuration = Startup.Configuration;
        string reCaptchResponse = value.ToString();
        string reCaptchaSecret = configuration["GoogleReCaptcha:SecretKey"];
        IHttpContextAccessor httpContextAccessor = validationContext.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor;
        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("secret", reCaptchaSecret),
            new KeyValuePair<string, string>("response", reCaptchResponse),
            new KeyValuePair<string, string>("remoteip", httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString())
        });

        HttpClient httpClient = new HttpClient();
        var httpResponse = httpClient.PostAsync("https://www.google.com/recaptcha/api/siteverify", content).Result;
        if (httpResponse.StatusCode != HttpStatusCode.OK)
        {
            return errorResult.Value;
        }

        String jsonResponse = httpResponse.Content.ReadAsStringAsync().Result;
        dynamic jsonData = JObject.Parse(jsonResponse);
        if (jsonData.success != true.ToString().ToLower())
        {
            return errorResult.Value;
        }

        return ValidationResult.Success;

    }
}

这个问题是由于DataAnnotationsValidator call AddDataAnnotationsValidation时没有将IServiceProvider传递给ValidationContext造成的。

对于这个问题,您可以查看Make dependency resolution available for EditContext form validation so that custom validators can access services. #11397

private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
    var validationContext = new ValidationContext(editContext.Model);
    var validationResults = new List<ValidationResult>();
    Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);

    // Transfer results to the ValidationMessageStore
    messages.Clear();
    foreach (var validationResult in validationResults)
    {
        foreach (var memberName in validationResult.MemberNames)
        {
            messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
        }
    }

    editContext.NotifyValidationStateChanged();
}

对于解决方法,您可以实现自己的 DataAnnotationsValidatorAddDataAnnotationsValidation

按照以下步骤操作:

  1. 自定义DataAnnotationsValidator

    public class DIDataAnnotationsValidator: DataAnnotationsValidator
    {
        [CascadingParameter] EditContext DICurrentEditContext { get; set; }
    
        [Inject]
        protected IServiceProvider ServiceProvider { get; set; }
        protected override void OnInitialized()
        {
            if (DICurrentEditContext == null)
            {
                throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
                    $"inside an EditForm.");
            }
    
            DICurrentEditContext.AddDataAnnotationsValidationWithDI(ServiceProvider);
        }
    }
    
  2. 自定义EditContextDataAnnotationsExtensions

    public static class EditContextDataAnnotationsExtensions
    {
        private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> _propertyInfoCache
        = new ConcurrentDictionary<(Type, string), PropertyInfo>();
    
        public static EditContext AddDataAnnotationsValidationWithDI(this EditContext editContext, IServiceProvider serviceProvider)
        {
            if (editContext == null)
            {
                throw new ArgumentNullException(nameof(editContext));
            }
    
            var messages = new ValidationMessageStore(editContext);
    
            // Perform object-level validation on request
            editContext.OnValidationRequested +=
                (sender, eventArgs) => ValidateModel((EditContext)sender, serviceProvider, messages);
    
            // Perform per-field validation on each field edit
            editContext.OnFieldChanged +=
                (sender, eventArgs) => ValidateField(editContext, serviceProvider, messages, eventArgs.FieldIdentifier);
    
            return editContext;
        }
        private static void ValidateModel(EditContext editContext, IServiceProvider serviceProvider,ValidationMessageStore messages)
        {
            var validationContext = new ValidationContext(editContext.Model, serviceProvider, null);
            var validationResults = new List<ValidationResult>();
            Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
    
            // Transfer results to the ValidationMessageStore
            messages.Clear();
            foreach (var validationResult in validationResults)
            {
                foreach (var memberName in validationResult.MemberNames)
                {
                    messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
                }
            }
    
            editContext.NotifyValidationStateChanged();
        }
    
        private static void ValidateField(EditContext editContext, IServiceProvider serviceProvider, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
        {
            if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
            {
                var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
                var validationContext = new ValidationContext(fieldIdentifier.Model, serviceProvider, null)
                {
                    MemberName = propertyInfo.Name
                };
                var results = new List<ValidationResult>();
    
                Validator.TryValidateProperty(propertyValue, validationContext, results);
                messages.Clear(fieldIdentifier);
                messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage));
    
                // We have to notify even if there were no messages before and are still no messages now,
                // because the "state" that changed might be the completion of some async validation task
                editContext.NotifyValidationStateChanged();
            }
        }
    
        private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, out PropertyInfo propertyInfo)
        {
            var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
            if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
            {
                // DataAnnotations only validates public properties, so that's all we'll look for
                // If we can't find it, cache 'null' so we don't have to try again next time
                propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
    
                // No need to lock, because it doesn't matter if we write the same value twice
                _propertyInfoCache[cacheKey] = propertyInfo;
            }
    
            return propertyInfo != null;
        }
    
    }
    
  3. DataAnnotationsValidator替换为DIDataAnnotationsValidator

    <EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
        @*<DataAnnotationsValidator />*@
        <DIDataAnnotationsValidator />
        <ValidationSummary />    
    </EditForm>
    
  4. 对于IHttpContextAccessor,您需要像

    一样在Startup.cs注册
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();
    
        services.AddHttpContextAccessor();
    }