数据验证是任何 Web 应用的关键组成部分。它有助于防止安全漏洞、数据损坏以及使用用户输入时可能出现的各种其他问题。
本文将探讨什么是数据验证以及它为何如此重要。我们将比较客户端验证和服务器端验证,并解释为什么不应仅依赖客户端验证。
然后,我们将介绍一些我在 Laravel 应用中常用的便捷验证规则。最后,我们将学习如何创建自己的验证规则并进行测试,以确保其按预期工作。
数据验证是在尝试使用数据之前检查数据有效性的过程。这可以是检查简单的项目,例如请求中是否存在必填字段,也可以是更复杂的检查,例如字段是否与特定模式匹配或在数据库中是否唯一。
通常,在 Web 应用中验证数据时,如果数据无效,您需要向用户返回错误消息。
这有助于防止安全漏洞、数据损坏并提高数据准确性。因此,只有在数据有效的情况下,我们才会继续处理请求。
记住,不能信任来自用户的任何数据(至少在您验证它之前!)。
数据验证之所以重要,原因有很多,包括:
在您的应用中验证数据最重要的原因之一是提升安全性。通过在使用数据之前对其进行验证,您可以降低恶意输入被用来攻击您的应用或用户的可能性。
想象一下,我们期望某个字段是整数,但用户却传递了一个文件。当我们尝试在应用的其他地方使用该数据时,这可能会导致各种问题。
再举一个例子,假设您正在构建一个允许用户对投票进行投票的 Web 应用。只能在 AppModelsPoll
模型上指定的 opens_at
时间和 closes_at
时间之间对投票进行投票。如果有人设置投票时不小心将 closes_at
时间设置为早于 opens_at
时间,会发生什么情况?根据您在应用中如何处理这种情况,这可能会导致各种问题。
通过在将数据存储到模型之前对其进行验证,我们可以提高应用中的数据准确性,并减少存储不正确数据的可能性。
除了能够验证 HTTP 请求中传递的数据外,您还可以验证 Artisan 命令。这可以防止开发人员意外输入无效值并导致应用出现问题。
通常,您可以在应用中使用两种类型的验证:客户端验证和服务器端验证。
客户端验证是在将数据发送到服务器之前在浏览器中执行的验证。它可以使用 JavaScript 甚至 HTML 属性来实现。
例如,我们可以向 HTML 中的数字字段添加一些简单的验证,以确保用户输入的数字介于 1 和 10 之间:
<input type="number" min="1" max="10" required>
此输入字段有四个单独的部分,对客户端验证很有用:
type="number"
:这告诉浏览器输入应为数字。在大多数浏览器上,这将阻止用户输入数字以外的任何内容。在移动设备上,它甚至可能会调出数字键盘而不是常规键盘,这对用户体验非常有益。min="1"
:这告诉浏览器输入的数字必须至少为 1。max="10"
:这告诉浏览器输入的数字最多必须为 10。required
:这告诉浏览器该字段是必需的,并且必须在提交表单之前填写。在大多数浏览器中,如果用户尝试使用无效值(或根本没有值)提交表单,浏览器将阻止提交表单,并向用户显示错误消息或提示。
这对指导用户和改善应用的整体用户体验非常有益。但这只是它应该被视为:一个指南。您不应仅依赖客户端验证作为应用中唯一的验证形式。
如果有人在浏览器中打开开发者工具,他们可以轻松删除和绕过您已设置的客户端验证。
此外,重要的是要记住,当恶意用户试图攻击您的应用时,他们通常会使用自动化脚本将请求直接发送到您的服务器。这意味着您已设置的客户端验证将被绕过。
服务器端验证是在服务器上的应用后端中运行的验证。在 Laravel 应用的上下文中,这通常是在控制器或表单请求类中运行的验证。
由于验证位于您的服务器上,用户无法更改,因此这是真正确保发送到服务器的数据有效的唯一方法。
因此,务必在您的应用中始终启用服务器端验证。理想情况下,您尝试从请求中读取的每个字段都应在尝试使用它执行任何业务逻辑之前进行验证。
现在我们已经了解了什么是验证以及它为什么重要,让我们来看看如何在 Laravel 中使用它。
如果您已经使用 Laravel 一段时间了,您就会知道 Laravel 具有内置于框架中的惊人的验证系统。因此,在您的应用中开始使用验证非常容易。
在 Laravel 中验证数据有几种常见方法,但我们将介绍两种最常见的方法:
要手动验证数据(例如在控制器方法中),您可以使用 IlluminateSupportFacadesValidator
facade 并调用 make
方法。
然后,我们可以将两个参数传递给 make
方法:
data
- 我们要验证的数据rules
- 我们要根据其验证数据的规则旁注:make
方法还接受两个可选参数:messages
和 attributes
。这些可用于自定义返回给用户的错误消息,但我们不会在本篇文章中介绍它们。
让我们来看一个您可能想要验证两个字段的示例:
<input type="number" min="1" max="10" required>
在上面的示例中,我们可以看到我们正在验证两个字段:title
和 body
。我们已对这两个字段的值进行硬编码以使示例更清晰,但在实际项目中,您通常会从请求中获取这些字段。我们正在检查 title
字段是否已设置、是字符串以及最大长度为 100 个字符。我们还检查 description
字段是否已设置、是字符串以及最大长度为 250 个字符。
创建验证器后,我们可以调用返回的 IlluminateValidationValidator
实例上的方法。例如,要检查验证是否失败,我们可以调用 fails
方法:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
同样,我们也可以在验证器实例上调用 validate
方法:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
如果验证失败,此 validate
方法将引发 IlluminateValidationValidationException
。Laravel 将根据所进行的请求类型自动处理此异常(假设您没有更改应用中的默认异常处理)。如果请求是 Web 请求,Laravel 将使用会话中的错误将用户重定向回上一页以供您显示。如果请求是 API 请求,Laravel 将返回 422 Unprocessable Entity
响应,其中包含验证错误的 JSON 表示形式,如下所示:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
在 Laravel 应用中验证数据的另一种常用方法是使用表单请求类。表单请求类是扩展 IlluminateFoundationHttpFormRequest
的类,用于对传入请求运行授权检查和验证。
我发现它们是保持控制器方法整洁的好方法,因为 Laravel 会在运行控制器方法的代码之前自动对请求中传递的数据运行验证。因此,我们不需要自己记住在验证器实例上运行任何方法。
让我们来看一个简单的例子。假设我们有一个基本的 AppHttpControllersUserController
控制器,其中有一个 store
方法允许我们创建一个新用户:
<input type="number" min="1" max="10" required>
在控制器方法中,我们可以看到我们接受 AppHttpRequestsUsersStoreUserRequest
表单请求类(我们将在后面介绍)作为方法参数。这将向 Laravel 指示我们希望在通过 HTTP 请求调用此方法时自动运行此请求类中的验证。
然后,我们在控制器方法中使用请求实例上的 validated
方法从请求中获取已验证的数据。这意味着它只会返回已验证的数据。例如,如果我们尝试在控制器中保存新的 profile_picture
字段,则也必须将其添加到表单请求类中。否则,validated
方法将不会返回它,因此 $request->validated('profile_picture')
将返回 null
。
现在让我们来看看 AppHttpRequestsUsersStoreUserRequest
表单请求类:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
我们可以看到请求类包含两个方法:
authorize
:此方法用于确定用户是否有权发出请求。如果该方法返回 false
,则会向用户返回 403 Forbidden
响应。如果该方法返回 true
,则将运行验证规则。rules
:此方法用于定义应在请求上运行的验证规则。该方法应返回一个应在请求上运行的规则数组。在 rules
方法中,我们指定 name
字段必须设置、必须是字符串并且最大长度必须为 100 个字符。我们还指定 email
字段必须设置、必须是电子邮件并且在 users
表(在 email
列上)中必须唯一。最后,我们指定 password
字段必须设置并且必须通过我们已设置的默认密码验证规则(我们稍后将介绍密码验证)。
如您所见,这是将验证逻辑与控制器逻辑分离的好方法,我发现它使代码更易于阅读和维护。
正如我已经提到的,Laravel 验证系统非常强大,可以轻松地将验证添加到您的应用中。
在本节中,我们将快速介绍一些我喜欢的便捷验证规则,我认为大多数用户都会发现它们在他们的应用中很有用。
如果您有兴趣查看 Laravel 中可用的所有规则,可以在 Laravel 文档中找到它们://m.sbmmt.com/link/45d5c43856059a4f97d43d6534be52d0
您需要运行的一种常见验证类型是验证数组。这可以是从验证传递的 ID 数组是否全部有效,到验证请求中传递的对象数组是否全部具有某些字段。
让我们来看一个如何验证数组的示例,然后我们将讨论正在执行的操作:
<input type="number" min="1" max="10" required>
在上面的示例中,我们传递的是一个对象数组,每个对象都有一个 name
和 email
字段。
对于验证,我们首先定义 users
字段已设置并且是数组。然后,我们指定数组的每个项目(使用 users.*
定向)都是一个包含 name
和 email
字段的数组。
然后,我们指定 name
字段(使用 users.*.name
定向)必须设置、必须是字符串并且不能超过 100 个字符。我们还指定 email
字段(使用 users.*.email
定向)必须设置、必须是电子邮件并且在 users
表的 email
列上必须唯一。
通过能够在验证规则中使用 *
通配符,我们可以轻松验证应用中的数据数组。
Laravel 提供了一些您可以使用的便捷日期验证规则。首先,要验证某个字段是否为有效日期,您可以使用 date
规则:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
如果您更喜欢检查日期是否采用特定格式,您可以使用 date_format
规则:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
您可能需要检查日期是否早于或晚于另一个日期。例如,假设您的请求中包含 opens_at
和 closes_at
字段,并且您要确保 closes_at
晚于 opens_at
并且 opens_at
晚于或等于今天。您可以像这样使用 after
规则:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
在上面的示例中,我们可以看到我们已将 today
作为参数传递给 opens_at
字段的 after
规则。Laravel 将尝试使用 strtotime
函数将此字符串转换为有效的 DateTime
对象并将其与该对象进行比较。
对于 closes_at
字段,我们将 opens_at
作为参数传递给 after_or_equal
规则。Laravel 将自动检测这是另一个正在验证的字段,并将这两个字段相互比较。
同样,Laravel 还提供 before
和 before_or_equal
规则,您可以使用它们来检查日期是否早于另一个日期:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
作为 Web 开发人员,我们的工作是尽力帮助用户在线安全。我们可以做到这一点的一种方法是在我们的应用中推广良好的密码实践,例如要求密码具有一定的长度、包含某些字符等。
Laravel 通过提供一个 IlluminateValidationRulesPassword
类来简化我们的工作,我们可以使用该类来验证密码。
它带有一些我们可以链接在一起的方法,以构建我们想要的密码验证规则。例如,假设我们希望用户的密码符合以下条件:
我们的验证可能如下所示:
<input type="number" min="1" max="10" required>
如示例所示,我们正在使用可链接的方法来构建我们想要的密码验证规则。但是,如果我们在多个不同的地方使用这些规则(例如注册、重置密码、在您的帐户页面上更新密码等),并且我们需要更改此验证以强制执行至少 12 个字符,会发生什么情况?我们需要遍历使用这些规则的所有地方并更新它们。
为了简化此操作,Laravel 允许我们定义一组默认的密码验证规则,我们可以在整个应用中使用它们。我们可以通过在我们的 AppProvidersAppServiceProvider
的 boot
方法中像这样使用 Password::defaults()
方法定义一组默认规则:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
执行此操作后,我们现在可以在验证规则中调用 Password::defaults()
,并将我们在 AppServiceProvider
中指定的规则用于:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
我参与过的几乎每个项目都包含某种形式的颜色选择器。无论是用户为其个人资料选择颜色、页面一部分的背景颜色还是其他内容,它都是经常出现的内容。
过去,我不得不使用正则表达式(我承认我不太了解)来验证颜色是否为十六进制格式的有效颜色(例如 #FF00FF
)。但是,Laravel 现在有一个方便的 hex_color
可以使用:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
如果您通过服务器将文件上传到应用,则需要在尝试存储文件之前验证文件是否有效。正如您所想象的那样,Laravel 提供了一些您可以使用的文件验证规则。
假设您想允许用户上传 PDF(.pdf)或 Microsoft Word(.docx)文件。验证可能如下所示:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
在代码示例中,我们可以看到我们正在验证文件类型,还设置了一些最小和最大文件大小限制。我们使用 types
方法来指定我们想要允许的文件类型。
min
和 max
方法还可以接受包含其他后缀的字符串,这些后缀指示文件大小单位。例如,我们还可以使用:
10kb
10mb
10gb
10tb
此外,我们还可以使用 IlluminateValidationRulesFile
类上的 image
方法来确保文件是图像:
<input type="number" min="1" max="10" required>
在上面的示例中,我们正在验证文件是否为图像,设置一些最小和最大文件大小限制,还设置一些最大尺寸(500 x 500 像素)。
您可能希望对应用中的文件上传采取不同的方法。例如,您可能希望直接从用户的浏览器上传到云存储(例如 S3)。如果您更喜欢这样做,您可能需要查看我的《使用 FilePond 在 Laravel 中上传文件》文章,该文章向您展示了如何执行此操作、您可能需要采取的不同验证方法以及如何对其进行测试。
您可能要进行的另一个常见检查是确保数据库中存在某个值。
例如,假设您的应用中有一些用户,并且您已创建了一个路由,以便您可以将它们批量分配给团队。因此,在您的请求中,您可能需要验证请求中传递的 user_ids
是否全部存在于 users
表中。
为此,您可以使用 exists
规则并传递您想要检查值是否存在其中的表名:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
在上面的示例中,我们正在检查 user_ids
数组中传递的每个 ID 是否都存在于 users
表的 id
列中。
这是确保您正在使用的数据有效并且在尝试使用它之前存在于数据库中的一种好方法。
如果您想更进一步,可以将 where
子句应用于 exists
规则以进一步过滤运行的查询:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
在上面的示例中,我们正在检查 user_ids
数组中传递的每个 ID 是否都存在于 users
表的 id
列中,并且用户的 is_verified
列设置为 true
。因此,如果我们传递未经验证的用户 ID,则验证将失败。
与 exists
规则类似,Laravel 还提供了一个 unique
规则,您可以使用它来检查数据库中的值是否唯一。
例如,假设您有一个 users
表,并且您要确保 email
字段唯一。您可以像这样使用 unique
规则:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
在上面的示例中,我们正在检查 email
字段是否已设置、是电子邮件并且在 users
表的 email
列上是唯一的。
但是,如果我们尝试在用户可以更新其电子邮件地址的个人资料页面上使用此验证,会发生什么情况?验证将失败,因为 users
表中存在一行包含用户尝试更新到的电子邮件地址。在这种情况下,我们可以使用 ignore
方法在检查唯一性时忽略用户 ID:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
如果您确实选择使用 ignore
方法,则应确保阅读 Laravel 文档中的此警告:
"您永远不应将任何用户控制的请求输入传递到 ignore
方法中。相反,您只应传递系统生成的唯一 ID,例如来自 Eloquent 模型实例的自增 ID 或 UUID。否则,您的应用将容易受到 SQL 注入攻击。"
也可能有时您想向 unique
规则添加其他 where
子句。您可能需要这样做以确保电子邮件地址对于特定团队是唯一的(这意味着不同团队中的另一个用户可以使用相同的电子邮件)。您可以通过将闭包传递给 where
方法来做到这一点:
<input type="number" min="1" max="10" required>
尽管 Laravel 附带了大量内置验证规则,但您可能需要创建自定义验证规则以适应特定用例。
谢天谢地,这在 Laravel 中也很容易做到!
让我们来看看如何构建自定义验证规则、如何使用它,然后如何为它编写测试。
出于本文的目的,我们不太关心我们正在验证的内容。我们只想了解创建自定义验证规则的一般结构以及如何对其进行测试。因此,我们将创建一个简单的规则来检查字符串是否为回文。
如果您不知道,回文是一个单词、短语、数字或其他字符序列,其正向和反向读取相同。例如,“racecar”是一个回文,因为如果您反转字符串,它仍然是“racecar”。而“laravel”不是回文,因为如果您反转字符串,它将是“levaral”。
要开始,我们将首先通过在项目路由中运行以下命令来创建一个新的验证规则:
use Illuminate\Support\Facades\Validator; $validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] );
这应该为我们创建了一个新的 App/Rules/Palindrome.php
文件:
$validator = Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] ); if ($validator->fails()) { // 一个或多个字段验证失败。 // 在此处进行处理... }
当运行规则时,Laravel 将自动调用 validate
方法。该方法接受三个参数:
$attribute
:正在验证的属性的名称。$value
:正在验证的属性的值。$fail
:如果验证失败,您可以调用的闭包。因此,我们可以在 validate
方法中添加我们的验证逻辑,如下所示:
Validator::make( data: [ 'title' => 'Blog Post', 'description' => 'Blog post description', ], rules: [ 'title' => ['required', 'string', 'max:100'], 'description' => ['required', 'string', 'max:250'], ] )->validate();
在上面的规则中,我们只是检查传递给规则的值是否与其反转的值相同。如果不是,我们将使用错误消息调用 $fail
闭包。这将导致该字段的验证失败。如果验证通过,则规则将不执行任何操作,我们可以继续使用我们的应用。
现在我们已经创建了规则,我们可以像这样在应用中使用它:
{ "message": "The title field is required. (and 1 more error)", "errors": { "title": [ "The title field is required." ], "description": [ "The description field is required." ] } }
尽管这是我们为演示目的而创建的简单规则,但希望这能让您了解如何为您的应用构建更复杂的规则。
就像应用中的任何其他代码一样,重要的是要测试您的验证规则以确保它们按预期工作。否则,您可能会冒使用不按预期工作的规则的风险。
为了了解如何做到这一点,让我们来看看如何测试我们在上一节中创建的回文规则。
对于此特定规则,我们要测试两种情况:
在更复杂的规则中,您可能会有更多的情况,但出于本文的目的,我们将其保持简单。
我们将在 tests/Unit/Rules
目录中创建一个名为 PalindromeTest.php
的新测试文件。
让我们来看看测试文件,然后我们将讨论正在执行的操作:
<input type="number" min="1" max="10" required>
在上面的测试文件中,我们定义了两个测试:rule_passes_with_a_valid_value
和 rule_fails_with_an_invalid_value
。
正如测试名称所暗示的那样,第一个测试确保当值为回文时规则通过,第二个测试确保当值不是回文时规则失败。
我们使用 PHPUnitFrameworkAttributesDataProvider
属性为测试提供有效值和无效值的列表以进行测试。这是保持测试整洁并能够使用相同测试检查多个值的好方法。例如,如果有人向 validValues
方法添加新的有效值,则测试将自动针对该值运行。
在 rule_passes_with_a_valid_value
测试中,我们使用有效值调用规则上的 validate
方法。我们将闭包传递给 fail
参数(如果规则内部验证失败,则调用此参数)。我们已指定如果执行闭包(即验证失败),则测试应失败。如果我们在没有执行闭包的情况下到达测试的结尾,那么我们就知道规则通过了,并且可以添加简单的断言 assertTrue(true)
来通过测试。
在 rule_fails_with_an_invalid_value
测试中,我们与第一个测试一样,但这次我们将无效值传递给规则。我们已指定如果执行闭包(即验证失败),则测试应通过,因为我们期望调用闭包。如果我们在没有执行闭包的情况下到达测试的结尾,则不会执行任何断言,并且 PHPUnit 应该为我们触发警告。但是,如果您更喜欢更明确地确保测试失败而不是仅仅给出错误,您可能需要采取略微不同的方法来编写测试。
在本文中,我们研究了什么是验证以及它为什么重要。我们比较了客户端验证和服务器端验证,并探讨了为什么客户端验证不应仅用作应用中唯一的验证形式。
我们还介绍了一些我喜欢在我的 Laravel 应用中使用的便捷验证规则。最后,我们探讨了如何创建您自己的验证规则并对其进行测试以确保其按预期工作。
希望您现在应该有足够的信心开始使用更多验证来提高应用的安全性和可靠性。
以上是Laravel验证的最终指南的详细内容。更多信息请关注PHP中文网其他相关文章!