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

yduiuuwa  于 2023-06-25  发布在  其他
关注(0)|答案(3)|浏览(116)

我们正在构建一个需要精确度的API端点。我们希望对发送到服务器的POST/PUT参数实施严格的验证。
如果API用户发送一个key=value对不支持(例如我们允许参数[first_name,last_name],用户包含不支持的参数[country]),我们希望验证失败。
我尝试过构建一个名为allowed_attributes(用作allowed_attributes:attr1,attr2,...)的自定义验证器,但要使其在$validationRules数组中可用,必须将其应用于嵌套/子属性列表的父属性(...因为否则我们的自定义验证器无法访问正在验证的属性)。

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属性列表(在自定义验证器中),有什么想法/建议吗?

7gcisfzg

7gcisfzg1#

我更喜欢发布一个新的答案,因为方法与前一个不同,而且更干净。因此,我宁愿将这两种方法分开,而不是在同一个答案中混合在一起。

更好的问题处理

自从我上次回答后,在深入研究了Validation的命名空间的源代码后,我发现最简单的方法是扩展Validator类来完成passes()函数,以检查您需要的内容。
这种实现的好处是还可以正确地处理单个数组/对象字段的特定错误消息,而无需任何effor,并且应该与通常的错误消息转换完全兼容。

创建自定义验证器类

首先在app文件夹中创建一个Validator类(我把它放在app/Validation/Validator.php下),然后像这样实现passs方法:

<?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方法上添加额外的检查。检查通过转换为点表示法(以支持数组/对象验证)的输入属性和至少分配了一个规则属性之间的键计算数组差异。

替换容器中默认的Validator

那么您错过的最后一步就是在服务提供商的** Boot 方法中绑定新的Validator类。要做到这一点,您只需覆盖绑定到IoC容器中的Illuminate\Validation\Factory类的解析器**作为'validator'

// 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中测试过。

x9ybnkn6

x9ybnkn62#

它应该适用于简单的键/值对与此自定义验证器:

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.*规则将自定义验证器应用于“父”对象的每个键。

回答您的问题

只是不要将请求 Package 在对象中,而是使用与上面相同的概念,并使用*应用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规则的参数列表中删除具有进一步验证逻辑的属性。
例如,如果您希望在parent对象中只要求 * first_name * 和 * last_name *,并禁止任何其他字段,则可以使用以下规则:

$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'
];

但是,以下 * WON'T * 工作如预期:

$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的验证逻辑,如果你要验证一个对象数组,这个自定义验证器可以工作,但是你得到的错误消息将是数组项上的通用错误消息,而不是不允许的数组项的键。
例如,您允许在请求中包含一个products字段,每个字段都有一个id:

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

如果你像这样验证一个请求:

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

您将在产品2上得到一个错误,但您将无法分辨哪个字段导致了问题!

uhry853o

uhry853o3#

看起来Laravel 8.50增加了一个方法,它不会抛出异常,而是简单地抛出任何没有被验证器指定的条目。

// In a service provider
Validator::excludeUnvalidatedArrayKeys();

参见https://github.com/laravel/framework/pull/37943

相关问题