Laravel验证:仅允许已知的属性/属性,否则验证失败

时间:2019-04-03 20:23:14

标签: php laravel validation

我们正在构建需要精确度的api端点。我们要对POST / PUT到服务器的参数实施严格的验证。

如果api用户发送了不支持的key=value对(例如,我们允许参数[first_name,last_name],并且用户包括不受支持的参数[country]),则我们希望验证失败。

曾尝试构建一个名为allowed_attributes(用作allowed_attributes:attr1,attr2,...)的自定义验证器,但要使其在$validationRules数组中可用,必须将其应用于a的父项嵌套/子属性的列表(...否则,我们的自定义验证器将无法访问正在验证的属性)。

Validator::extend('allowed_attributes', 'App\Validators\AllowedAttributesValidator@validate');

这与其他验证器一起产生了问题,然后我们必须在其中预期该父/子结构和周围的代码,包括在验证后对错误密钥和错误消息字符串的额外清理。

tl; dr:非常脏,不是干净的实现。

$validationRules = [
  'parent' => 'allowed_attributes:first_name,last_name',
  'parent.first_name' => 'required|string|max:40',
  'parent.last_name' => 'required|string|max:40'
];

$isValid = Validator::make(['parent' => $request], $validationRules);

var_dump("Validation results: " . ($isValid ? "passed" : "failed"));

关于如何在laravel中更干净地完成此操作的任何想法/建议,而无需使用父/子关系来访问所有$ request属性的列表(在自定义验证程序中)?

2 个答案:

答案 0 :(得分:0)

使用此自定义验证程序,它应可用于简单的键/值对:

Validator::extendImplicit('allowed_attributes', function ($attribute, $value, $parameters, $validator) {
    // If the attribute to validate request top level
    if (strpos($attribute, '.') === false) {
        return in_array($attribute, $parameters);
    }

    // If the attribute under validation is an array
    if (is_array($value)) {
        return empty(array_diff_key($value, array_flip($parameters)));
    }

    // If the attribute under validation is an object
    foreach ($parameters as $parameter) {
        if (substr_compare($attribute, $parameter, -strlen($parameter)) === 0) {
            return true;
        }
    }

    return false;
});

验证器逻辑非常简单:

  • 如果$attribute不包含.,则说明我们正在处理一个顶级参数,我们只需要检查一下它是否存在于我们allowed_attributes列表中通过规则。
  • 如果$attribute的值是一个数组,则将输入键与allowed_attributes列表进行比较,并检查是否还有属性键。如果是这样,我们的请求有一个我们没想到的额外密钥,因此我们返回false
  • 否则,$attribute的值是一个对象,我们必须检查我们期望的每个参数(再次是allowed_attributes列表)是否是当前属性的最后一个部分(如laravel给我们的那样) $attribute中完整的点标记属性)。

此处的关键是将其应用于应遵循的验证规则(请注意第一个验证规则):

$validationRules = [
  'parent.*' => 'allowed_attributes:first_name,last_name',
  'parent.first_name' => 'required|string|max:40',
  'parent.last_name' => 'required|string|max:40'
];

parent.*规则会将自定义验证器应用于“父”对象的每个键。

回答您的问题

请勿将请求包装在一个对象中,而是使用与上述相同的概念,并在allowed_attributes处应用*规则:

$validationRules = [
  '*' => 'allowed_attributes:first_name,last_name',
  'first_name' => 'required|string|max:40',
  'last_name' => 'required|string|max:40'
];

这会将规则应用于所有当前的顶级输入请求字段。


注意::请记住,laravel验证在放入规则数组时会受到规则顺序的影响。 例如,将parent.*规则移到底部将触发parent.first_nameparent.last_name上的规则;相反,将其保留为第一条规则不会触发first_namelast_name的验证。

这意味着您最终可以从allowed_attributes规则的参数列表中删除具有进一步验证逻辑的属性。

例如,如果您只需要 first_name last_name 并禁止parent对象中的任何其他字段,则可以使用以下规则:

$validationRules = [
  // This will be triggered for all the request fields except first_name and last_name
  'parent.*' => 'allowed_attributes', 
  'parent.first_name' => 'required|string|max:40',
  'parent.last_name' => 'required|string|max:40'
];

但是,以下不会会按预期工作:

$validationRules = [
  'parent.first_name' => 'required|string|max:40',
  'parent.last_name' => 'required|string|max:40',
  // This, instead would be triggered on all fields, also on first_name and last_name
  // If you put this rule as last, you MUST specify the allowed fields.
  'parent.*' => 'allowed_attributes', 
];

阵列次要问题

据我所知,根据Laravel的验证逻辑,如果您要验证对象数组,则此自定义验证器将起作用,但是您将获得的错误消息将在数组项上通用,而不是键上不允许的那个数组项。

例如,您在请求中允许一个产品字段,每个字段的ID为:

$validationRules = [
  'products.*' => 'allowed_attributes:id',
];

如果您验证这样的请求:

{
    "products": [{
        "id": 3
    }, {
        "id": 17,
        "price": 3.49
    }]
}

您会在产品2上看到一个错误,但是您将无法确定是哪个字段引起了该问题!

答案 1 :(得分:0)

我更喜欢发布一个新的答案,因为该方法与以前的方法有所不同,并且更加简洁。因此,我宁愿将两种方法分开,而不是在同一答案中混在一起。

更好的问题处理方式

自从我上次回答以来,对Validation命名空间的源代码进行了更深入的研究后,我发现最简单的方法是扩展Validator类以补充passes()函数来检查所需的内容。

此实现的好处是还可以正确处理单个数组/对象字段的特定错误消息,而无需费力,并且应与常规错误消息转换完全兼容。

创建自定义验证器类

您应该首先在您的应用文件夹中创建一个 Validator 类(我将其放置在app/Validation/Validator.php下),并实现如下的 pass 方法:

<?php

namespace App\Validation;

use Illuminate\Support\Arr;
use Illuminate\Validation\Validator as BaseValidator;

class Validator extends BaseValidator
{
    /**
     * Determine if the data passes the validation rules.
     *
     * @return bool
     */
    public function passes()
    {
        // Perform the usual rules validation, but at this step ignore the
        // return value as we still have to validate the allowance of the fields
        // The error messages count will be recalculated later and returned.
        parent::passes();

        // Compute the difference between the request data as a dot notation
        // array and the attributes which have a rule in the current validator instance
        $extraAttributes = array_diff_key(
            Arr::dot($this->data),
            $this->rules
        );

        // We'll spin through each key that hasn't been stripped in the
        // previous filtering. Most likely the fields will be top level
        // forbidden values or array/object values, as they get mapped with
        // indexes other than asterisks (the key will differ from the rule
        // and won't match at earlier stage).
        // We have to do a deeper check if a rule with that array/object
        // structure has been specified.
        foreach ($extraAttributes as $attribute => $value) {
            if (empty($this->getExplicitKeys($attribute))) {
                $this->addFailure($attribute, 'forbidden_attribute', ['value' => $value]);
            }
        }

        return $this->messages->isEmpty();
    }
}

这实际上将扩展默认的 Validator 类,以在passs方法上添加其他检查。该检查通过键计算输入属性(转换为点表示法(以支持数组/对象验证)和至少分配了一个规则的属性之间的键)之间的数组差异。< / p>

替换容器中的默认验证器

那么,您错过的最后一步是在服务提供商的 boot 方法中绑定新的Validator类。为此,您可以覆盖Illuminate\Validation\Factory类的解析器,该类以'validator'绑定到IoC容器中:

// Do not forget the class import at the top of the file!
use App\Validation\Validator;

// ...

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->make('validator')
            ->resolver(function ($translator, $data, $rules, $messages, $attributes) {
                return new Validator($translator, $data, $rules, $messages, $attributes);
            });
    }

// ...

在控制器中的实际使用

您无需执行任何特定操作即可使用此功能。只需照常调用validate方法:

$this->validate(request(), [
    'first_name' => 'required|string|max:40',
    'last_name' => 'required|string|max:40'
]);

自定义错误消息

要自定义错误消息,您只需在lang文件中添加一个翻译键,其键号等于forbidden_attribute(您可以在addFailure的自定义Validator类中自定义错误键名称。方法调用)。

示例: resources/lang/en/validation.php

<?php

return [
    // ...

    'forbidden_attribute' => 'The :attribute key is not allowed in the request body.',

    // ...
];

注意:该实现仅在Laravel 5.3中进行了测试。