Laravel 启动指南(四)
原文:
zh.annas-archive.org/md5/d0c72cd35a2ef551cf4f36bed0d4e4e2译者:飞龙
第九章:用户认证与授权
设置基本的用户认证系统,包括注册、登录、会话、密码重置和访问权限,通常是创建应用程序基础的更耗时的部分之一。这是将功能提取到库中的一个主要候选项,而且有许多这样的库可供选择。
但由于项目的认证需求可能存在较大差异,大多数认证系统很快就会变得笨重且难以使用。幸运的是,Laravel 已经找到了一种方法,可以创建一套易于使用和理解的认证系统,同时灵活到可以适应各种设置。
Laravel 的每一个新安装都包含一个 create_users_table 迁移和一个内置的 User 模型。如果引入了 Breeze(参见 “Laravel Breeze”)或 Jetstream(参见 “Laravel Jetstream”),它们将为您的应用程序提供一系列与认证相关的视图、路由、控制器/动作和其他功能。API 是清晰易懂的,所有约定都协同工作,提供了一个简单且无缝的认证和授权系统。
用户模型与迁移
当您创建一个新的 Laravel 应用程序时,您将看到的第一个迁移和模型是 create_users_table 迁移和 App\User 模型。示例 9-1 直接展示了从迁移中获取的 users 表中的字段。
示例 9-1. Laravel 的默认用户迁移
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
我们有一个自增主键 ID,一个名称,一个唯一的电子邮件,一个密码,一个“记住我”令牌,以及创建和修改的时间戳。这涵盖了大多数应用程序中处理基本用户认证所需的一切内容。
认证与授权的区别
认证 意味着验证某人是谁,并允许他们在您的系统中以此身份行事。这包括登录和注销过程,以及任何允许用户在使用应用程序期间识别自己的工具。
授权 意味着确定经过身份验证的用户是否被允许(授权)执行特定行为。例如,授权系统允许您禁止非管理员查看站点的收入情况。
User 模型略微复杂,您可以在 示例 9-2 中看到。App\User 类本身很简单,但它扩展了 Illuminate\Foundation\Auth\User 类,后者引入了几个特性。
示例 9-2. Laravel 的默认 User 模型
<?php
// App\User
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
<?php
// Illuminate\Foundation\Auth\User
namespace Illuminate\Foundation\Auth;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword, MustVerifyEmail;
}
Eloquent 模型刷新器
如果这些内容对您完全陌生,请考虑在继续学习如何使用 Eloquent 模型之前阅读第五章。
那么,我们从这个模型中能学到什么?首先,用户存储在 users 表中;Laravel 将从类名推断出这一点。创建新用户时,我们可以填写 name、email 和 password 属性,而在将用户输出为 JSON 时,则会排除 password 和 remember_token 属性。目前看起来一切都很好。
我们还可以从 Illuminate\Foundation\Auth 版本的 User 中的合约和特性中看到,框架中有一些功能(例如身份验证、授权和密码重置的能力),理论上可以应用于其他模型,而不仅仅是 User 模型,并且可以单独或集体应用。
Authenticatable 合约要求方法(例如 getAuthIdentifier()),允许框架对此模型的实例进行身份验证到身份验证系统;Authenticatable 特性包含了满足普通 Eloquent 模型此合约所需的方法。
Authorizable 合约要求一个方法 (can()),允许框架在不同上下文中授权此模型的实例以获取其访问权限。毫不奇怪,Authorizable 特性提供了方法,这些方法将为普通的 Eloquent 模型满足 Authorizable 合约。
最后,CanResetPassword 合约要求方法 (getEmailForPasswordReset()、sendPasswordResetNotification()),允许框架重置任何满足此合约的实体的密码。CanResetPassword 特性提供了方法,以满足普通 Eloquent 模型的这一合约。
到目前为止,我们能够轻松地在数据库中表示个别用户(通过迁移),并使用可以进行身份验证(登录和注销)、授权(检查对特定资源的访问权限)和发送密码重置电子邮件的模型实例。
使用 auth() 全局辅助函数和 Auth 门面
auth() 全局辅助函数是在整个应用程序中与已验证用户的状态交互的最简单方法。您还可以注入一个 Illuminate\Auth\AuthManager 实例并获得相同的功能,或者使用 Auth 门面。
最常见的用法是检查用户是否已登录(如果当前用户已登录,则 auth()->check() 返回 true;如果用户未登录,则 auth()->guest() 返回 true)以及获取当前已登录用户(使用 auth()->user(),或仅获取 ID 使用 auth()->id();如果没有用户登录,则两者都返回 null)。
查看示例 Example 9-3 了解控制器中全局辅助函数的示例用法。
示例 9-3. 在控制器中使用 auth() 全局辅助函数的示例用法
public function dashboard()
{
if (auth()->guest()) {
return redirect('sign-up');
}
return view('dashboard')
->with('user', auth()->user());
}
路由 routes/auth.php,Auth 控制器和 Auth 操作
如果你正在使用 Laravel 的其中一个入门工具包,你会发现使用内置的身份验证路由(例如登录、注册和重置密码)需要路由、控制器和视图。
Breeze 和 Jetstream 都使用自定义路由文件定义您的路由:routes/auth.php。它们并不完全相同,但可以查看 示例 9-4 以了解 Breeze 的认证路由文件的一部分,以便了解它们的一般情况。
示例 9-4. Breeze 的路由/auth.php 的一部分
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])
->name('password.store');
});
Breeze 在 Auth 命名空间下发布控制器,您可以根据需要进行配置:
-
AuthenticatedSessionController.php
-
ConfirmablePasswordController.php
-
EmailVerificationNotificationController.php
-
EmailVerificationPromptController.php
-
NewPasswordController.php
-
PasswordController.php
-
PasswordResetLinkController.php
-
RegisteredUserController.php
-
VerifyEmailController.php
Jetstream(以及它依赖的 Fortify)不发布控制器,而是发布您可以自定义的“操作”:
app/Actions/Fortify/CreateNewUser.php
app/Actions/Fortify/PasswordValidationRules.php
app/Actions/Fortify/ResetUserPassword.php
app/Actions/Fortify/UpdateUserPassword.php
app/Actions/Fortify/UpdateUserProfileInformation.php
app/Actions/Jetstream/DeleteUser.php
Breeze 和 Jetstream 的前端模板
到此为止,您的认证系统已经有了迁移、模型、控制器/操作和路由。但是您的视图呢?
您可以在 “Laravel Breeze” 和 “Laravel Jetstream” 中了解更多信息,但每个工具都提供多种不同的堆栈,并且每个堆栈将其模板放置在不同的位置。
一般来说,基于 JavaScript 的堆栈将其模板放置在 resources/js 中,而基于 Blade 的堆栈将其放置在 resources/views 中。
每个功能(登录、注册、重置密码等)至少有一个视图,并且它们都采用了流畅的基于 Tailwind 的设计生成,可以直接使用或自定义。
“记住我”
Breeze 和 Jetstream 都已经默认实现了此功能,但是了解其工作原理以及如何在自己的项目中使用仍然是值得的。如果您想要实现“记住我”风格的长期访问令牌,请确保您的 users 表中有一个 remember_token 列(如果您使用了默认迁移,那么这个列应该已经存在)。
当您正常登录用户时(这是 LoginController 使用 AuthenticatesUsers trait 所做的方式),您将“尝试”使用用户提供的信息进行认证,就像在 示例 9-5 中所示。
示例 9-5. 尝试用户认证
if (auth()->attempt([
'email' => request()->input('email'),
'password' => request()->input('password'),
])) {
// Handle the successful login
}
这为您提供了一个与用户会话同久的用户登录。如果您希望 Laravel 使用 Cookie 无限期延长登录时间(只要用户在同一台计算机上且不退出登录),您可以将布尔值 true 作为 auth()->attempt() 方法的第二个参数传递。查看 示例 9-6 以了解该请求的外观。
示例 9-6. 使用“记住我”复选框进行用户认证尝试
if (auth()->attempt([
'email' => request()->input('email'),
'password' => request()->input('password'),
], request()->filled('remember'))) {
// Handle the successful login
}
您可以看到,我们检查了输入是否具有非空(“filled”)remember 属性,该属性将返回一个布尔值。这允许我们的用户通过登录表单中的复选框决定是否要记住登录状态。
后来,如果你需要手动检查当前用户是否通过记住令牌进行了认证,有一个方法可以做到:auth()->viaRemember() 返回一个布尔值,指示当前用户是否通过记住令牌进行了认证。这使你可以防止通过记住令牌访问某些更高敏感度功能;而是,你可以要求用户重新输入他们的密码。
密码确认
在你的应用程序的某些部分访问之前,用户可能需要重新确认他们的密码。例如,如果用户已经登录了一段时间,然后尝试访问你站点的账单部分,你可能希望他们验证他们的密码。
你可以在你的路由上附加 password.confirm 中间件来强制这种行为。一旦他们确认了密码,用户将被发送到他们最初尝试访问的路由。此后,用户在 3 小时内不需要重新确认密码;你可以在 auth.password_timeout 配置设置中更改这个时间。
手动认证用户
用户认证的最常见情况是,允许用户提供他们的凭证,然后使用 auth()->attempt() 来查看提供的凭证是否与任何真实用户匹配。如果匹配,则登录他们。
但有时候,在某些情境下,你能够选择自己选择性地登录一个用户,这是非常有价值的。例如,你可能希望允许管理员用户切换用户。
有四种方法可以实现这一点。首先,你可以只传递一个用户 ID:
auth()->loginUsingId(5);
其次,你可以传递一个 User 对象(或者任何实现 Illuminate\Contracts\Auth\Authenticatable 合约的对象):
auth()->login($user);
第三和第四,你可以选择仅为当前请求验证给定用户,这不会影响你的会话或者 cookie,可以使用 once() 或 onceUsingId():
auth()->once(['username' => 'mattstauffer']);
// or
auth()->onceUsingId(5);
请注意,你传递给 once() 方法的数组可以包含任何键值对来唯一标识你想要认证的用户。如果适合你的项目,你甚至可以传递多个键和值。例如:
auth()->once([
'last_name' => 'Stauffer',
'zip_code' => 90210,
])
手动登出用户
如果你需要手动登出用户,只需调用 logout():
auth()->logout();
使其他设备上的会话失效
如果你想要在任何其他设备上登出用户的当前会话 —— 例如,在他们更改密码后 —— 你需要提示用户输入他们的密码并将其传递给 logoutOtherDevices() 方法。为此,你需要将 auth.session 中间件应用到你想让他们退出登录的所有路由上(对于大多数项目而言,这是整个应用程序)。
然后你可以在任何需要的地方内联使用它:
auth()->logoutOtherDevices($password);
如果你想让用户详细查看其他活动会话,Jetstream(参见“Laravel Jetstream”)默认提供了一个页面,列出所有活动会话,并提供一个按钮可以登出所有会话。
认证中间件
在示例 9-3 中,您看到如何检查访客是否已登录,并在未登录时重定向他们。您可以在应用程序的每个路由上执行这些检查,但很快会变得乏味。事实证明,路由中间件(详见第 10 章以了解其工作原理)非常适合将某些路由限制为仅限访客或经过身份验证的用户。
再次,Laravel 默认即可提供我们所需的中间件。您可以查看您在App\Http\Kernel中定义的路由中间件:
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
六种默认的路由中间件与身份验证相关:
auth
限制路由访问权限仅限经过身份验证的用户
auth.basic
通过 HTTP 基本身份验证限制仅限经过身份验证的用户访问
auth.session
使路由可供其他设备登出使用Auth::logoutOtherDevices
can
用于授权用户访问指定路由
guest
限制未经身份验证的用户访问
password.confirm
要求用户最近重新确认其密码
对于需要仅限经过身份验证的用户访问的部分,最常见的做法是使用auth,而对于不希望经过身份验证的用户看到的任何路由(如登录表单),则使用guest。auth.basic和auth.session用于认证的中间件则较少使用。
示例 9-7 展示了几个由auth中间件保护的路由示例。
示例 9-7. 受auth中间件保护的示例路由
Route::middleware('auth')->group(function () {
Route::get('account', [AccountController::class, 'dashboard']);
});
Route::get('login', [LoginController::class, 'getLogin'])->middleware('guest');
电子邮件验证
如果您希望要求用户验证他们注册时使用的电子邮件地址的访问权限,则可以使用 Laravel 的电子邮件验证功能。
要启用电子邮件验证,请更新您的App\User类,并使其实现Illuminate\Contracts\Auth\MustVerifyEmail合同,如示例 9-8 所示。
示例 9-8. 将MustVerifyEmail特性添加到Authenticatable模型中
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
// ...
}
users表还必须包含一个名为email_verified_at的可空时间戳列,这是默认的CreateUsersTable迁移已经为您提供的。
最后,您需要在控制器中启用电子邮件验证路由。最简单的方法是在路由文件中使用Auth::routes(),并将verify参数设置为true:
Auth::routes(['verify' => true]);
现在,您可以保护任何希望不被尚未验证其电子邮件地址的任何用户访问的路由:
Route::get('posts/create', function () {
// Only verified users may enter...
})->middleware('verified');
您可以自定义在验证后重定向用户的路由Verification``Controller:
protected $redirectTo = '/profile';
Blade 身份验证指令
如果您想检查用户是否经过身份验证,而不是在路由级别进行检查,而是在视图中进行检查,您可以使用@auth和@guest(参见示例 9-9)。
示例 9-9. 在模板中检查用户的身份验证状态
@auth
// The user is authenticated
@endauth
@guest
// The user is not authenticated
@endguest
您还可以通过将守卫名称作为参数传递给这两种方法来指定您想要使用的守卫,如示例 9-10 所示。
示例 9-10. 在模板中检查特定认证保护的身份验证
@auth('trainees')
// The user is authenticated
@endauth
@guest('trainees')
// The user is not authenticated
@endguest
守卫
Laravel 认证系统的每个方面都通过称为守卫的东西路由。每个守卫由两个部分组成:定义它如何持久化和检索认证状态的驱动程序(例如 session),以及允许你按某些条件获取用户的提供者(例如 users)。
开箱即用,Laravel 有两个守卫:web 和 api。web 是更传统的认证样式,使用 session 驱动程序和基本用户提供者。api 使用相同的用户提供者,但它使用 token 驱动程序而不是 session 在每个请求中进行认证。
如果你想以不同方式处理用户身份的识别和持久性(例如,从长时间运行的会话更改为每页加载提供的令牌),你会更改驱动程序;如果你想更改用户的存储类型或检索方法(例如,将用户存储在 Mongo 而不是 MySQL 中),你会更改提供者。
更改默认守卫
守卫在 config/auth.php 中定义,你可以在那里更改它们、添加新的守卫,并定义默认的守卫。就其价值而言,这是一种相对不常见的配置;大多数 Laravel 应用程序只使用一个守卫。
“默认”守卫是在没有指定守卫的情况下使用任何认证功能时将使用的守卫。例如,auth()->user() 将使用默认守卫拉取当前认证的用户。你可以通过更改 config/auth.php 中的 auth.defaults.guard 设置来更改此守卫:
'defaults' => [
'guard' => 'web', // Change the default here
'passwords' => 'users',
],
配置约定
你可能已经注意到,我用 auth.defaults.guard 等引用来引用配置部分。这意味着在 config/auth.php 中,在以 defaults 键为键的数组部分中,应该有一个以 guard 为键的属性。
在不更改默认值的情况下使用其他守卫
如果你想使用另一个守卫但不更改默认值,你可以在 auth() 调用中以 guard() 开头:
$apiUser = auth()->guard('api')->user();
这将在此调用中仅获取使用 api 守卫的当前用户。
添加新的守卫
你可以随时在 config/auth.php 的 auth.guards 设置中添加新的守卫:
'guards' => [
'trainees' => [
'driver' => 'session',
'provider' => 'trainees',
],
],
在这里,我们创建了一个新的守卫(除了 web 和 api)名为 trainees。假设在接下来的这一节中,我们正在构建一个应用程序,其中我们的用户是体育教练,每个教练都有自己的用户——受训者——他们可以登录到他们的子域名。因此,我们需要一个单独的守卫来处理他们。
driver 的唯二选项是 token 和 session。开箱即用,provider 的唯一选项是 users,支持对默认的 users 表进行认证,但你可以轻松创建自己的提供者。
闭包请求守卫
如果您想定义一个自定义守卫,并且您的守卫条件(如何查找给定用户的请求)可以简单地在任何给定的 HTTP 请求中响应,您可能只想将用户查找代码放入一个闭包中,而不必创建一个新的自定义守卫类。
viaRequest() 认证方法允许仅通过闭包(定义在第二个参数中)来定义一个守卫(第一个参数中命名的),该闭包接受 HTTP 请求并返回适当的用户。要在 AuthServiceProvider 的 boot() 方法中注册一个闭包请求守卫,如 示例 9-11 所示。
示例 9-11. 定义闭包请求守卫
public function boot(): void
{
Auth::viaRequest('token-hash', function ($request) {
return User::where('token-hash', $request->token)->first();
});
}
创建自定义用户提供程序
在 config/auth.php 中定义守卫的位置下方,有一个 auth.providers 部分,定义了可用的提供程序。让我们创建一个名为 trainees 的新提供程序:
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
'trainees' => [
'driver' => 'eloquent',
'model' => App\Trainee::class,
],
],
driver 的两个选项是 eloquent 和 database。如果使用 eloquent,您将需要一个包含 Eloquent 类名的 model 属性(用于 User 类的模型);如果使用 database,则需要一个 table 属性来定义应该对其进行身份验证的表。
在我们的示例中,您可以看到此应用程序有一个 User 和一个 Trainee,它们需要分别进行身份验证。这样,代码可以区分 auth()->guard('users') 和 auth()->guard('trainees')。
最后一点:auth 路由中间件可以接受一个参数,即守卫名称。因此,您可以使用特定的守卫保护某些路由:
Route::middleware('auth:trainees')->group(function () {
// Trainee-only routes here
});
非关系数据库的自定义用户提供程序
刚才描述的用户提供程序创建流程仍然依赖于相同的 UserProvider 类,这意味着它期望从关系数据库中提取标识信息。但是,如果您使用的是 Mongo 或 Riak 或类似的东西,实际上您需要创建自己的类。
要做到这一点,请创建一个新的类,实现 Illuminate\Contracts\Auth\UserProvider 接口,然后在 AuthServiceProvider@boot 中绑定它:
auth()->provider('riak', function ($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...
return new RiakUserProvider($app['riak.connection']);
});
认证事件
我们将在 第十六章 中更多地讨论事件,但 Laravel 的事件系统是一个基本的发布/订阅框架。有系统生成的事件和用户生成的事件进行广播,并且用户可以创建事件监听器以响应某些事件。
那么,如果您想在用户因登录尝试失败次数过多而被锁定后,每次都向特定的安全服务发送一个 ping 呢?也许此服务监视某些地理区域的某个特定数量的登录失败或其他内容。当然,您可以在适当的控制器中注入一个调用。但是通过事件,您可以创建一个监听器来监听“用户被锁定”的事件,并注册它。
查看 示例 9-12 以查看身份验证系统发出的所有事件。
示例 9-12. 框架生成的认证事件
protected $listen = [
'Illuminate\Auth\Events\Attempting' => [],
'Illuminate\Auth\Events\Authenticated' => [],
'Illuminate\Auth\Events\CurrentDeviceLogout' => [],
'Illuminate\Auth\Events\Failed' => [],
'Illuminate\Auth\Events\Lockout' => [],
'Illuminate\Auth\Events\Login' => [],
'Illuminate\Auth\Events\Logout' => [],
'Illuminate\Auth\Events\OtherDeviceLogout' => [],
'Illuminate\Auth\Events\PasswordReset' => [],
'Illuminate\Auth\Events\Registered' => [],
'Illuminate\Auth\Events\Validated' => [],
'Illuminate\Auth\Events\Verified' => [],
];
如你所见,有“用户注册”、“用户尝试登录”、“用户验证但未登录”、“用户已认证”、“成功登录”、“登录失败”、“登出”、“从其他设备登出”、“从当前设备登出”、“锁定”、“重置密码”和“用户邮箱验证”等监听器。要了解如何为这些事件构建事件监听器,请查看第十六章。
授权与角色
最后,让我们介绍一下 Laravel 的授权系统。它使你能够确定用户是否被授权执行特定操作,你将使用几个主要动词进行检查:can、cannot、allows和denies。
大部分授权控制都将使用Gate外观进行,但在你的控制器、User模型、中间件和 Blade 指令中也有便捷的辅助功能可用。查看示例 9-13 可以体验我们能做到什么。
示例 9-13. Gate外观的基本用法
if (Gate::denies('edit-contact', $contact)) {
abort(403);
}
if (! Gate::allows('create-contact', Contact::class)) {
abort(403);
}
定义授权规则
定义授权规则的默认位置是在AuthServiceProvider的boot()方法中,在这里你将调用Auth外观的方法。
授权规则称为ability,由两部分组成:一个字符串键(例如,update-contact)和返回布尔值的闭包。示例 9-14 展示了更新联系人的 ability。
示例 9-14. 用于更新联系人的样例 ability
class AuthServiceProvider extends ServiceProvider
{
public function boot(): void
{
Gate::define('update-contact', function ($user, $contact) {
return $user->id == $contact->user_id;
});
}
}
让我们来看看定义 ability 的步骤。
首先,你需要定义一个键。在命名这个键时,你应该考虑在你的代码流中哪个字符串对于引用你所提供给用户的 ability 是有意义的。你可以在示例 9-14 中看到代码使用了{*verb*}-{*modelName*}的约定:create-contact、update-contact等。
其次,你要定义闭包。第一个参数将是当前已认证的用户,之后的所有参数将是你要检查访问权限的对象——在本例中是联系人。
因此,考虑到这两个对象,我们可以检查用户是否有权限更新这个联系人。你可以按自己的逻辑编写这段代码,但在我们查看的应用中(在示例 9-14 中),授权取决于是否是联系人行的创建者。如果当前用户创建了联系人,闭包将返回true(授权),否则返回false(未授权)。
就像路由定义一样,你也可以使用类和方法而不是闭包来解析这个定义:
$gate->define('update-contact', 'ContactACLChecker@updateContact');
Gate外观(和注入Gate)
现在你已经定义了一个 ability,是时候测试它了。最简单的方法是使用Gate外观,就像在示例 9-15 中一样(或者你可以注入Illuminate\Contracts\Auth\Access\Gate的实例)。
示例 9-15. Gate外观的基本用法
if (Gate::allows('update-contact', $contact)) {
// Update contact
}
// or
if (Gate::denies('update-contact', $contact)) {
abort(403);
}
您可能还可以定义一个具有多个参数的能力 —— 也许联系人可以分组,并且您希望授权用户是否有权限将联系人添加到组中。Example 9-16 展示了如何做到这一点。
Example 9-16. 具有多个参数的能力
// Definition
Gate::define('add-contact-to-group', function ($user, $contact, $group) {
return $user->id == $contact->user_id && $user->id == $group->user_id;
});
// Usage
if (Gate::denies('add-contact-to-group', [$contact, $group])) {
abort(403);
}
如果您需要检查不是当前认证用户的用户的授权,请尝试forUser(),就像 Example 9-17 中那样。
Example 9-17. 指定Gate的用户
if (Gate::forUser($user)->denies('create-contact')) {
abort(403);
}
资源门
访问控制列表最常见的用途是定义对单个“资源”的访问权限(想想一个 Eloquent 模型,或者您允许用户从其管理面板管理的东西)。
resource() 方法使得可以一次将四个最常见的门控(view、create、update 和 delete)应用于单个资源:
Gate::resource('photos', 'App\Policies\PhotoPolicy');
这相当于定义以下内容:
Gate::define('photos.view', 'App\Policies\PhotoPolicy@view');
Gate::define('photos.create', 'App\Policies\PhotoPolicy@create');
Gate::define('photos.update', 'App\Policies\PhotoPolicy@update');
Gate::define('photos.delete', 'App\Policies\PhotoPolicy@delete');
授权中间件
如果您想授权整个路由,可以使用Authorize中间件(有一个can的快捷方式),就像在 Example 9-18 中那样。
Example 9-18. 使用Authorize中间件
Route::get('people/create', function () {
// Create a person
})->middleware('can:create-person');
Route::get('people/{person}/edit', function () {
// Edit person
})->middleware('can:edit,person');
这里,{person} 参数(无论它是作为字符串定义还是作为绑定路由模型)将作为附加参数传递给能力方法。
Example 9-18 中的第一个检查是一个普通的能力,但第二个是一个策略,我们将在“策略”中讨论它。
如果您需要检查不需要模型实例的操作(例如 create,与 edit 不同,不会传递实际的路由模型绑定实例),您可以只传递类名:
Route::post('people', function () {
// Create a person
})->middleware('can:create,App\Person');
控制器授权
Laravel 中的父类 App\Http\Controllers\Controller 导入了 AuthorizesRequests 特性,提供了三种授权方法:authorize()、authorizeForUser() 和 authorizeResource()。
authorize() 接受一个能力键和一个对象(或对象数组)作为参数,如果授权失败,它将以 403(未经授权)状态码退出应用程序。这意味着这个特性可以将三行授权代码转换为一行,正如您在 Example 9-19 中所看到的。
Example 9-19. 使用authorize()简化控制器授权
// From this:
public function edit(Contact $contact)
{
if (Gate::cannot('update-contact', $contact)) {
abort(403);
}
return view('contacts.edit', ['contact' => $contact]);
}
// To this:
public function edit(Contact $contact)
{
$this->authorize('update-contact', $contact);
return view('contacts.edit', ['contact' => $contact]);
}
authorizeForUser() 是相同的,但允许您传递一个 User 对象,而不是默认为当前认证用户:
$this->authorizeForUser($user, 'update-contact', $contact);
authorizeResource() 在控制器构造函数中调用一次,将预定义的一组授权规则映射到该控制器中的每个 RESTful 控制器方法 —— 类似于 Example 9-20。
Example 9-20. authorizeResource() 方法的授权到方法映射
...
class ContactController extends Controller
{
public function __construct()
{
// This call does everything you see in the methods below.
// If you put this here, you can remove all authorize()
// calls in the individual resource methods here.
$this->authorizeResource(Contact::class);
}
public function index()
{
$this->authorize('viewAny', Contact::class);
}
public function create()
{
$this->authorize('create', Contact::class);
}
public function store(Request $request)
{
$this->authorize('create', Contact::class);
}
public function show(Contact $contact)
{
$this->authorize('view', $contact);
}
public function edit(Contact $contact)
{
$this->authorize('update', $contact);
}
public function update(Request $request, Contact $contact)
{
$this->authorize('update', $contact);
}
public function destroy(Contact $contact)
{
$this->authorize('delete', $contact);
}
}
检查用户实例
如果您不在控制器中,更有可能检查特定用户的能力而不是当前认证的用户。使用Gate外观可以使用forUser()方法实现这一点,但有时语法可能有些奇怪。
User 类上的 Authorizable 特性提供了四种方法来实现更可读的授权功能:$user->can()、$user->canAny()、$user->cant() 和 $user->cannot()。你可以大概猜到,cant() 和 cannot() 是一样的,而 can() 则完全相反。使用 canAny(),你传递一个权限数组,该方法检查用户是否可以执行其中任何一个。
这意味着你可以做像 示例 9-21 这样的事情。
示例 9-21. 检查 User 实例的授权
$user = User::find(1);
if ($user->can('create-contact')) {
// Do something
}
在幕后,这些方法只是将参数传递给 Gate;在前面的示例中,Gate::forUser($user)->check('create-contact')。
Blade 检查
Blade 还有一个小方便的助手:@can 指令。示例 9-22 展示了它的使用方式。
示例 9-22. 使用 Blade 的 @can 指令
<nav>
<a href="/">Home</a>
@can('edit-contact', $contact)
<a href="{{ route('contacts.edit', [$contact->id]) }}">Edit This Contact</a>
@endcan
</nav>
你还可以在 @can 和 @endcan 之间使用 @else,以及像 示例 9-23 中使用 @cannot 和 @endcannot。
示例 9-23. 使用 Blade 的 @cannot 指令
<h1>{{ $contact->name }}</h1>
@cannot('edit-contact', $contact)
LOCKED
@endcannot
拦截检查
如果你曾经用过管理员用户类构建过应用程序,你可能已经看过本章节中所有简单授权闭包,并考虑过如何添加一个超级用户类,在任何情况下都覆盖这些检查。幸运的是,已经有一个工具可以做到这一点。
在 AuthServiceProvider 中,你已经在定义你的能力,你还可以添加一个 before() 检查,该检查在所有其他检查之前运行,可以选择性地覆盖它们,就像 示例 9-24 中一样。
示例 9-24. 使用 before() 覆盖 Gate 检查
Gate::before(function ($user, $ability) {
if ($user->isOwner()) {
return true;
}
});
注意,也会传递能力的字符串名称,因此你可以根据你的能力命名方案区分你的 before() 钩子。
策略
到目前为止,所有的访问控制都要求你手动将 Eloquent 模型与能力名称关联起来。你可以创建一个名为 visit-dashboard 的能力,该能力与特定的 Eloquent 模型无关,但你可能已经注意到,我们大多数示例都涉及对某物做某事,在大多数情况下,受到操作的某物是一个 Eloquent 模型。
授权策略是组织结构,帮助你根据你正在控制访问的资源将授权逻辑分组。它们使得能够轻松管理定义针对特定 Eloquent 模型(或其他 PHP 类)行为的授权规则,全部在一个地方。
生成策略
策略是 PHP 类,可以通过 Artisan 命令生成:
php artisan make:policy ContactPolicy
生成后,需要注册它们。AuthServiceProvider 有一个 $policies 属性,它是一个数组。每个项目的键是受保护资源的类名(几乎总是一个 Eloquent 类),值是策略类名。示例 9-25 显示了这将是什么样子。
示例 9-25. 在AuthServiceProvider中注册策略
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Contact::class => ContactPolicy::class,
];
由 Artisan 生成的策略类没有任何特殊属性或方法。但是您添加的每个方法现在都映射为此对象的能力键。
策略自动发现
Laravel 尝试“猜测”您的策略及其相应模型之间的链接。例如,它将自动将PostPolicy应用于您的Post模型。
如果您需要自定义 Laravel 用于猜测此映射的逻辑,请查看策略文档。
让我们定义一个update()方法来看看它的工作原理(参见示例 9-26)。
示例 9-26. 一个样本update()策略方法
<?php
namespace App\Policies;
class ContactPolicy
{
public function update($user, $contact)
{
return $user->id == $contact->user_id;
}
}
注意,该方法的内容看起来与在Gate定义中的内容完全相同。
不需要使用实例的政策方法
如果您需要定义一个与类相关但不是特定实例的策略方法——例如,“此用户是否可以创建任何联系人?”而不仅仅是“此用户是否可以查看此特定联系人?”——您可以像处理普通策略方法一样处理它:
...
class ContactPolicy
{
public function create($user)
{
return $user->canCreateContacts();
}
检查政策
如果为资源类型定义了策略,则Gate外观将使用第一个参数来确定要在策略上检查哪个方法。如果您运行Gate::allows('update', $contact),它将检查ContactPolicy@update方法的授权情况。
这也适用于Authorize中间件和User模型检查以及 Blade 检查,如示例 9-27 所示。
示例 9-27. 对策略进行授权检查
// Gate
if (Gate::denies('update', $contact)) {
abort(403);
}
// Gate if you don't have an explicit instance
if (! Gate::check('create', Contact::class)) {
abort(403);
}
// User
if ($user->can('update', $contact)) {
// Do stuff
}
// Blade
@can('update', $contact)
// Show stuff
@endcan
此外,还有一个policy()辅助程序,允许您检索策略类并运行其方法:
if (policy($contact)->update($user, $contact)) {
// Do stuff
}
覆盖政策
就像普通能力定义一样,政策可以定义一个before()方法,允许您在处理之前覆盖任何调用(参见示例 9-28)。
示例 9-28. 使用before()方法覆盖策略
public function before($user, $ability)
{
if ($user->isAdmin()) {
return true;
}
}
测试
应用程序测试经常需要代表特定用户执行特定行为。因此,在应用程序测试中进行身份验证并测试授权规则和身份验证路由是必要的。
当然,您可以编写一个应用程序测试,手动访问登录页面,然后填写表单并提交,但这并不是必需的。相反,最简单的选项是使用->be()方法模拟作为用户登录。请参阅示例 9-29。
示例 9-29. 在应用程序测试中作为用户进行身份验证
public function test_it_creates_a_new_contact()
{
$user = User::factory()->create();
$this->be($user);
$this->post('contacts', [
'email' => 'my@email.com',
]);
$this->assertDatabaseHas('contacts', [
'email' => 'my@email.com',
'user_id' => $user->id,
]);
}
您还可以使用并链actingAs()方法,而不是be(),如果您更喜欢其阅读方式:
public function test_it_creates_a_new_contact()
{
$user = User::factory()->create();
$this->actingAs($user)->post('contacts', [
'email' => 'my@email.com',
]);
$this->assertDatabaseHas('contacts', [
'email' => 'my@email.com',
'user_id' => $user->id,
]);
}
我们也可以像在示例 9-30 中那样测试授权。
示例 9-30. 测试授权规则
public function test_non_admins_cant_create_users()
{
$user = User::factory()->create([
'admin' => false,
]);
$this->be($user);
$this->post('users', ['email' => 'my@email.com']);
$this->assertDatabaseMissing('users', [
'email' => 'my@email.com',
]);
}
或者,我们可以像在示例 9-31 中那样测试 403 响应。
示例 9-31. 通过检查状态代码测试授权规则
public function test_non_admins_cant_create_users()
{
$user = User::factory()->create([
'admin' => false,
]);
$this->be($user);
$response = $this->post('users', ['email' => 'my@email.com']);
$response->assertStatus(403);
}
我们还需要测试我们的认证(注册和登录)路由是否正常工作,如示例 9-32 所示。
示例 9-32. 测试认证路由
public function test_users_can_register()
{
$this->post('register', [
'name' => 'Sal Leibowitz',
'email' => 'sal@leibs.net',
'password' => 'abcdefg123',
'password_confirmation' => 'abcdefg123',
]);
$this->assertDatabaseHas('users', [
'name' => 'Sal Leibowitz',
'email' => 'sal@leibs.net',
]);
}
public function test_users_can_log_in()
{
$user = User::factory()->create([
'password' => Hash::make('abcdefg123')
]);
$this->post('login', [
'email' => $user->email,
'password' => 'abcdefg123',
]);
$this->assertTrue(auth()->check());
$this->assertTrue($user->is(auth()->user()));
}
我们还可以使用集成测试功能来直接测试“点击”认证字段并“提交”字段以测试整个流程。关于这一点,我们将在第十二章中详细讨论。
TL;DR
在默认的User模型、create_users_table迁移以及 Jetstream 和 Breeze 之间,Laravel 提供了开箱即用的完整用户认证系统选项。Breeze 在控制器中处理认证功能,Jetstream 在操作中处理认证功能,两者都可以根据每个应用程序进行定制。这两个工具还发布了配置文件和模板以进行定制。
Auth门面和全局助手auth()提供了访问当前用户(auth()->user())的途径,并且轻松检查用户是否已登录(auth()->check()和auth()->guest())。
Laravel 还内置了一个授权系统,允许您定义特定的权限(create-contact、visit-secret-page)或为用户与整个模型的交互定义策略。
使用Gate门面、User类上的can()和cannot()方法、Blade 模板中的@can和@cannot指令、控制器中的authorize()方法或can中间件来检查授权。
第十章:请求、响应和中间件
我们已经谈论过 Illuminate Request对象了。例如,在第三章中,您看到如何在构造函数中使用类型提示来获取实例或使用request()助手来检索它,在第七章中我们讨论了如何使用它来获取关于用户输入的信息。
在本章中,您将了解更多关于Request对象的信息,它是如何生成的,代表什么以及它在应用程序生命周期中扮演的角色。我们还将讨论Response对象以及 Laravel 对中间件模式的实现。
Laravel 的请求生命周期
每个进入 Laravel 应用程序的请求,无论是由 HTTP 请求还是命令行交互生成的,都会立即转换为 Illuminate Request对象,然后跨越许多层并最终被应用程序本身解析。然后应用程序生成一个 Illuminate Response对象,该对象通过这些层级返回并最终返回给最终用户。
此请求/响应生命周期见图 10-1。让我们看看如何实现每一个步骤,从第一行代码到最后。
图 10-1. 请求/响应生命周期
引导应用程序
每个 Laravel 应用程序都在 Web 服务器级别设置了某种形式的配置,在 Apache 的*.htaccess文件或 Nginx 配置设置或类似的地方捕获每个 Web 请求,无论 URL 如何,并将其路由到 Laravel 应用程序目录中的public/index.php*。
index.php实际上并没有那么多代码。它有三个主要功能。
首先,它加载 Composer 的自动加载文件,注册所有由 Composer 加载的依赖项。
接下来,它启动 Laravel 的引导过程,创建应用程序容器(您将在第十一章中了解更多关于容器的信息),并注册一些核心服务(包括内核,我们马上会谈到)。
最后,它创建内核的一个实例,创建代表当前用户 Web 请求的请求,并将请求传递给内核进行处理。内核响应一个 Illuminate Response对象,index.php将其返回给最终用户。然后,内核终止页面请求。
内核是每个 Laravel 应用程序的核心路由器,负责接收用户请求,通过中间件处理它,处理异常并将其传递给页面路由器,然后返回最终响应。实际上,有两个内核,但每个页面请求只使用一个。一个路由器处理 Web 请求(HTTP 内核),另一个处理控制台、定时任务和 Artisan 请求(控制台内核)。每个都有一个handle()方法,负责接收 Illuminate Request对象并返回 Illuminate Response对象。
内核运行所有在每个请求之前需要运行的引导,包括确定当前请求运行的环境(测试、本地、生产等),以及运行所有服务提供商。HTTP 内核还定义了将包装每个请求的中间件列表,包括负责会话和 CSRF 保护的核心中间件。
服务提供商
尽管这些引导中有一些过程性代码,几乎所有 Laravel 的引导代码都被分离到 Laravel 称为服务提供商的东西中。服务提供商是一个类,封装了各个应用程序部分需要运行的逻辑,以引导它们的核心功能。
例如,有一个AuthServiceProvider,它引导所有 Laravel 认证系统所需的注册,并且有一个RouteServiceProvider,它引导路由系统。
服务提供商的概念一开始可能有点难以理解,所以可以这样考虑:你的应用程序中的许多组件都有引导代码,需要在应用程序初始化时运行。服务提供商是将这些引导代码分组到相关类中的工具。如果你有任何需要在应用程序代码正常工作之前运行的代码,它就是服务提供商的一个强力候选者。
例如,如果你发现你正在开发的功能需要在容器中注册一些类(你将在第十一章中了解更多),你会为该功能创建一个专门的服务提供商。你可能会有一个GitHubServiceProvider或MailerServiceProvider。
服务提供商有两个重要的方法:boot() 和 register()。还有一个你可能选择使用的DeferrableProvider接口。这里是它们的工作原理。
其次会调用所有服务提供商的boot()方法。现在你可以在这里做任何其他引导,比如绑定事件监听器或定义路由——任何依赖于整个 Laravel 应用程序已经引导的东西。
首先会调用所有服务提供商的register()方法。在这里,你可以将类和别名绑定到容器中。不要在register()中做任何依赖于整个应用程序已经引导的事情。
如果你的服务提供者只会在容器中注册绑定(即教会容器如何解析给定的类或接口),而不执行任何其他引导操作,你可以“延迟”它们的注册,这意味着它们不会运行,除非显式从容器请求它们的绑定。这可以加快应用程序的平均引导时间。
如果你想延迟你的服务提供者的注册,首先要实现 Illuminate\Contracts\Support\DeferrableProvider 接口;然后,给服务提供者一个 provides() 方法,返回该提供者提供的绑定列表,如示例 10-1 所示。
示例 10-1. 延迟服务提供者的注册
...
use Illuminate\Contracts\Support\DeferrableProvider;
class GitHubServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function provides()
{
return [
GitHubClient::class,
];
}
服务提供者的更多用途
服务提供者还有一套方法和配置选项,可以在作为 Composer 包的一部分发布时为最终用户提供高级功能。查看Laravel 源中的服务提供者定义,了解更多信息。
现在我们已经涵盖了应用程序引导,让我们来看看 Request 对象,这是引导过程中最重要的输出。
请求对象
Illuminate\Http\Request 类是 Laravel 特有的 Symfony HttpFoundation 的扩展。
Symfony HttpFoundation
Symfony 的 HttpFoundation 类组件几乎支持目前所有的 PHP 框架;这是 PHP 中表示 HTTP 请求、响应、头部、Cookie 等的最流行和强大的抽象集合。
Request 对象旨在表示你可能关心的用户 HTTP 请求的每个相关信息。
在原生 PHP 代码中,你可能会发现自己查看 $_SERVER、$_GET、$_POST 等全局变量和处理逻辑的组合,以获取关于当前用户请求的信息。用户上传了哪些文件?他们的 IP 地址是什么?他们提交了哪些字段?所有这些信息都分散在语言和代码中,这使得理解起来困难,而模拟起来更加困难。
Symfony 的 Request 对象将所有表示单个 HTTP 请求所需的信息集成到一个对象中,并添加了便捷方法来轻松获取有用的信息。Illuminate 的 Request 对象增加了更多便捷方法,用于获取它所代表的请求的信息。
捕获请求
在 Laravel 应用中,你几乎不太可能需要这样做,但如果你需要直接从 PHP 的全局变量中捕获自己的 Illuminate Request 对象,你可以使用 capture() 方法:
$request = Illuminate\Http\Request::capture();
在 Laravel 中获取请求对象
Laravel 为每个请求创建一个内部的 Request 对象,你可以通过几种方式来访问它。
首先—再次强调,我们将在第十一章中更详细地介绍—您可以在任何由容器解析的构造函数或方法中对类进行类型提示。这意味着您可以在控制器方法或服务提供程序中进行类型提示,就像在示例 10-2 中看到的那样。
示例 10-2. 在容器解析的方法中对类进行类型提示以接收Request对象
...
use Illuminate\Http\Request;
class PersonController extends Controller
{
public function index(Request $request)
{
$allInput = $request->all();
}
或者,您可以使用request()全局助手,允许您在其上调用方法(例如,request()->input()),也允许您单独调用它以获取$request的实例:
$request = request();
$allInput = $request->all();
// or
$allInput = request()->all();
最后,您可以使用app()全局方法来获取Request的实例。您可以传递完全限定的类名或简写request:
$request = app(Illuminate\Http\Request::class);
$request = app('request');
获取请求的基本信息
现在您知道如何获取Request的实例了,您可以做些什么呢?Request对象的主要目的是表示当前的 HTTP 请求,因此Request类提供的主要功能是轻松获取有关当前请求的有用信息。
我已经将这里描述的方法分类,但请注意分类之间肯定存在重叠,并且分类有点随意—例如,查询参数可以与“用户和请求状态”一样轻松地出现在“基本用户输入”中。希望这些分类能让您轻松了解可用内容,然后您可以丢弃这些分类。
还要注意,Request对象上还有许多其他可用的方法;这些只是最常用的方法。
基本用户输入
基本用户输入方法使得获取用户显式提供的信息变得简单—通常通过提交表单或 Ajax 组件。在这里提到“用户提供的输入”时,我指的是来自查询字符串(GET)、表单提交(POST)或 JSON 的输入。基本用户输入方法包括以下内容:
all()
返回所有用户提供的输入的数组。
input(*fieldName*)
返回单个用户提供的输入字段的值。
only(*fieldName*|[*array,of,field,names*])
返回指定字段名(们)的所有用户提供的输入的数组。
except(*fieldName*|[*array,of,field,names*])
返回除指定字段名(们)外的所有用户提供的输入的数组。
exists(*fieldName*)
返回一个布尔值,指示输入中是否存在指定字段。has()是其别名。在输入中存在指定字段时执行给定的回调。
filled(*fieldName*)
返回一个布尔值,指示输入中是否存在指定字段并且不为空(即具有值)。
whenFilled()
在输入中存在指定字段并且不为空(即具有值)时执行给定的回调。
json()
如果页面收到了 JSON,则返回一个ParameterBag。
boolean(*fieldName*)
将输入的值作为布尔值返回。将字符串和整数转换为适当的布尔值(使用 FILTER_VALIDATE_BOOLEAN)。如果请求中不存在键,则返回 false。
json(*keyName*)
返回从发送到页面的 JSON 中给定键的值。
示例 10-3 提供了如何使用请求中提供的用户信息方法的几个快速示例。
示例 10-3. 从请求获取基本用户提供的信息
// form
<form method="POST" action="/form">
@csrf
<input name="name"> Name<br>
<input type="submit">
</form>
// Route receiving the form
Route::post('form', function (Request $request) {
echo 'name is ' . $request->input('name') . '<br>';
echo 'all input is ' . print_r($request->all()) . '<br>';
echo 'user provided email address: ' . $request->has('email') ? 'true' : 'false';
});
用户和请求状态
用户和请求状态方法包括通过表单未显式提供的输入:
method()
返回用于访问此路由的方法(GET、POST、PATCH 等)。
path()
返回用于访问此页面的路径(不包括域名);例如,'http://www.myapp.com/abc/def' 将返回 'abc/def'。
url()
返回用于访问此页面的带域名的 URL;例如,'abc' 将返回 'http://www.myapp.com/abc'。
is()
返回布尔值,指示当前页面请求是否与提供的字符串模糊匹配(例如,/a/b/c 将被 $request->is('*b*') 匹配,其中 * 表示任意字符);使用在 Str::is() 中找到的自定义正则表达式解析器。
ip()
返回用户的 IP 地址。
header()
返回标题数组(例如 ['accept-language' => ['en-US,en;q=0.8']]),或者如果作为参数传递了标题名称,则只返回该标题。
server()
返回传统存储在 $_SERVER 中的变量数组(例如 REMOTE_ADDR),或者如果传递了 $_SERVER 变量名称,则只返回该值。
secure()
返回指示此页面是否使用 HTTPS 加载的布尔值。
pjax()
返回指示此页面请求是否使用了 Pjax 加载的布尔值。
wantsJson()
返回指示此请求的 Accept 头中是否有任何 /json 内容类型的布尔值。
isJson()
返回指示此页面请求的 Content-Type 头中是否有任何 /json 内容类型的布尔值。
accepts()
返回指示此页面请求是否接受给定内容类型的布尔值。
文件
到目前为止,我们讨论的所有输入都是显式的(通过诸如 all()、input() 等方法检索),或者由浏览器或引用站点定义(通过诸如 pjax() 等方法检索)。文件输入类似于显式用户输入,但处理方式有很大不同:
file()
返回所有已上传文件的数组,或者如果传递了键(文件上传字段名称),则仅返回一个文件。
allFiles()
返回所有已上传文件的数组;与 file() 相比,命名更清晰,非常有用。
hasFile()
返回指定键是否上传了文件的布尔值。
每个上传的文件都将是 Symfony\Component\HttpFoundation\File\UploadedFile 的实例,提供一套工具来验证、处理和存储上传的文件。
请查看 Chapter 14 获取有关如何处理上传文件的更多示例。
持久性
请求还可以提供与会话交互的功能。大多数会话功能存放在其他位置,但有几个方法对当前页面请求特别相关:
flash()
将当前请求的用户输入闪存到会话中以供稍后检索,这意味着它保存到会话中,但在下一个请求后消失。
flashOnly()
为提供的数组中的任何键闪存当前请求的用户输入。
flashExcept()
闪存当前请求的用户输入,除了提供的数组中的任何键。
old()
返回所有先前闪存的用户输入的数组,或者如果传递了键,则返回先前闪存的该键的值。
flush()
清除所有先前闪存的用户输入。
cookie()
从请求中检索所有 cookie,或者如果提供了键,则仅检索该 cookie。
hasCookie()
返回一个布尔值,指示请求是否具有给定键的 cookie。
flash*() 和 old() 方法用于存储用户输入,并在稍后检索它,通常在输入经过验证并被拒绝后。
响应对象
类似于 Request 对象,还有一个 Illuminate Response 对象,表示您的应用程序发送给最终用户的响应,包括标头、cookie、内容和用于发送最终用户浏览器的页面渲染指令的任何其他内容。
就像 Request 一样,Illuminate\Http\Response 类扩展了 Symfony 类:Symfony\Component\HttpFoundation\Response。这是一个基类,具有一系列属性和方法,使得表示和呈现响应成为可能;Illuminate 的 Response 类通过一些有用的快捷方式对其进行装饰。
在控制器中使用和创建响应对象
在谈论如何自定义您的 Response 对象之前,让我们退后一步,看看我们最常用的 Response 对象的工作方式。
最终,从路由定义返回的任何 Response 对象都将转换为 HTTP 响应。它可以定义特定的标头或特定的内容,设置 cookie 或其他任何内容,但最终它将转换为用户浏览器可以解析的响应。
让我们看一下最简单的响应,例如 Example 10-4。
示例 10-4. 最简单的可能的 HTTP 响应
Route::get('route', function () {
return new Illuminate\Http\Response('Hello!');
});
// Same, using global function:
Route::get('route', function () {
return response('Hello!');
});
我们创建一个响应,为其提供一些核心数据,然后返回它。我们还可以自定义 HTTP 状态、标头、cookie 等等,例如 Example 10-5。
示例 10-5. 具有自定义状态和标头的简单 HTTP 响应
Route::get('route', function () {
return response('Error!', 400)
->header('X-Header-Name', 'header-value')
->cookie('cookie-name', 'cookie-value');
});
设置标头
我们通过使用 header() 流畅方法在响应上定义一个标头,例如在 Example 10-5。第一个参数是标头名称,第二个是标头值。
添加 cookie
如果需要的话,我们还可以直接在Response对象上设置 Cookie。我们将在第 14 章更详细地讨论 Laravel 的 Cookie 处理,但你可以查看示例 10-6 了解如何将 Cookie 附加到响应中的简单用例。
示例 10-6. 将 Cookie 附加到响应
return response($content)
->cookie('signup_dismissed', true);
专用响应类型
还有一些专门用于视图、下载、文件和 JSON 的特殊响应类型。每种都是预定义的宏,可以轻松重用特定的头部或内容结构模板。
查看响应
在第 3 章中,我使用全局的view()助手展示如何返回模板,例如view('*view.name.here*')或类似的内容。但如果在返回视图时需要自定义头部、HTTP 状态或其他内容,可以使用view()响应类型,如示例 10-7 所示。
示例 10-7. 使用view()响应类型
Route::get('/', function (XmlGetterService $xml) {
$data = $xml->get();
return response()
->view('xml-structure', $data)
->header('Content-Type', 'text/xml');
});
下载响应
有时候你希望应用程序强制用户的浏览器下载一个文件,无论是在 Laravel 中创建文件还是从数据库或受保护的位置提供文件。使用download()响应类型可以轻松实现这一点。
必需的第一个参数是要浏览器下载的文件路径。如果是生成的文件,你需要将其暂时保存在某个地方。
可选的第二个参数是下载文件的文件名(例如,export.csv)。如果你不在这里传递一个字符串,文件名将会自动生成。可选的第三个参数允许你传递一个头部数组。示例 10-8 展示了使用download()响应类型的例子。
示例 10-8. 使用download()响应类型
public function export()
{
return response()
->download('file.csv', 'export.csv', ['header' => 'value']);
}
public function otherExport()
{
return response()->download('file.pdf');
}
如果希望在返回下载响应后从磁盘删除原始文件,可以在download()方法后链式调用deleteFileAfterSend()方法:
public function export()
{
return response()
->download('file.csv', 'export.csv')
->deleteFileAfterSend();
}
文件响应
文件响应类似于下载响应,不同之处在于它允许浏览器显示文件而不是强制下载。这在处理图片和 PDF 文件时最常见。
必需的第一个参数是文件名,可选的第二个参数可以是头部数组(参见示例 10-9)。
示例 10-9. 使用file()响应类型
public function invoice($id)
{
return response()->file("./invoices/{$id}.pdf", ['header' => 'value']);
}
JSON 响应
JSON 响应非常常见,尽管编程起来并不是特别复杂,但也有一个定制的响应类型。
JSON 响应将传递的数据转换为 JSON(使用json_encode()),并将Content-Type设置为application/json。你还可以选择使用setCallback()方法创建一个 JSONP 响应而不是 JSON,如示例 10-10 所示。
示例 10-10. 使用json()响应类型
public function contacts()
{
return response()->json(Contact::all());
}
public function jsonpContacts(Request $request)
{
return response()
->json(Contact::all())
->setCallback($request->input('callback'));
}
public function nonEloquentContacts()
{
return response()->json(['Tom', 'Jerry']);
}
重定向响应
重定向不常在response()辅助函数中调用,因此它们与我们已经讨论过的其他自定义响应类型有所不同,但它们仍然只是另一种响应。从 Laravel 路由返回的重定向会向用户发送一个重定向(通常是 301),将其导向另一个页面或返回到上一页。
你技术上可以从response()中调用重定向,例如return response()->redirectTo('/')。但更常见的做法是使用专门的全局辅助函数。
有一个全局的redirect()函数,用于创建重定向响应,还有一个全局的back()函数,是redirect()->back()的快捷方式。
就像大多数全局辅助函数一样,redirect()全局函数可以传递参数,也可以用来获取其类的实例,然后链式调用方法。如果不链式调用,而只是传递参数,redirect()的行为与redirect()->to()相同;它接受一个字符串并重定向到该字符串的 URL。示例 10-11 展示了其使用示例。
示例 10-11. 使用redirect()全局辅助函数的示例
return redirect('account/payment');
return redirect()->to('account/payment');
return redirect()->route('account.payment');
return redirect()->action('AccountController@showPayment');
// If redirecting to an external domain
return redirect()->away('https://tighten.co');
// If named route or controller needs parameters
return redirect()->route('contacts.edit', ['id' => 15]);
return redirect()->action('ContactController@edit', ['id' => 15]);
当处理和验证用户输入时,你也可以“返回”到上一页,这在验证上下文中特别有用。示例 10-12 展示了验证上下文中的常见模式。
示例 10-12. 带有输入的回跳重定向
public function store()
{
// If validation fails...
return back()->withInput();
}
最后,你可以同时重定向并向会话闪存数据。这在处理错误和成功消息时很常见,例如示例 10-13。
示例 10-13. 带有闪存数据的重定向
Route::post('contacts', function () {
// Store the contact
return redirect('dashboard')->with('message', 'Contact created!');
});
Route::get('dashboard', function () {
// Get the flashed data from session--usually handled in Blade template
echo session('message');
});
自定义响应宏
你也可以使用宏创建自己的自定义响应类型。这允许你定义要对响应及其提供的内容进行的一系列修改。
让我们重新创建json()自定义响应类型,只是为了看看它是如何工作的。如常,你应该为这类绑定创建一个自定义服务提供者,但现在我们暂时将其放在AppServiceProvider中,如示例 10-14 所示。
示例 10-14. 创建一个自定义响应宏
...
class AppServiceProvider
{
public function boot()
{
Response::macro('myJson', function ($content) {
return response(json_encode($content))
->withHeaders(['Content-Type' => 'application/json']);
});
}
然后,我们可以像使用预定义的json()宏一样使用它:
return response()->myJson(['name' => 'Sangeetha']);
这将返回一个带有数组主体的 JSON 编码响应,带有适当的 JSON 类型的Content-Type头。
负责任接口
如果你想要自定义如何发送响应,而宏提供的空间或组织不够,或者你希望你的对象能够根据自己的显示逻辑作为“响应”返回,那么Responsable接口适合你。
Responsable接口,Illuminate\Contracts\Support\Responsable,规定其实现类必须有一个toResponse()方法。这需要返回一个 Illuminate Response对象。示例 10-15 说明了如何创建一个Responsable对象。
示例 10-15. 创建一个简单的Responsable对象
...
use Illuminate\Contracts\Support\Responsable;
class MyJson implements Responsable
{
public function __construct($content)
{
$this->content = $content;
}
public function toResponse()
{
return response(json_encode($this->content))
->withHeaders(['Content-Type' => 'application/json']);
}
然后,我们可以像使用我们自定义的宏一样使用它:
return new MyJson(['name' => 'Sangeetha']);
相对于之前介绍的响应宏,这可能看起来需要做很多工作。但是在处理更复杂的控制器操作时,Responsable接口真正发挥作用。一个常见的例子是使用它来创建视图模型(或视图对象),就像在示例 10-16 中。
示例 10-16. 使用Responsable创建视图对象
...
use Illuminate\Contracts\Support\Responsable;
class GroupDonationDashboard implements Responsable
{
public function __construct($group)
{
$this->group = $group;
}
public function budgetThisYear()
{
// ...
}
public function giftsThisYear()
{
// ...
}
public function toResponse()
{
return view('groups.dashboard')
->with('annual_budget', $this->budgetThisYear())
->with('annual_gifts_received', $this->giftsThisYear());
}
在这种情况下,将复杂的视图准备工作移到一个专用的、可测试的对象中,并保持控制器的简洁,这开始变得更有意义。以下是使用那个Responsable对象的控制器:
...
class GroupController
{
public function index(Group $group)
{
return new GroupDonationsDashboard($group);
}
Laravel 和中间件
回顾一下图 10-1,这是本章的开头。
我们已经讨论了请求和响应,但实际上还没有深入了解中间件是什么。您可能已经熟悉中间件,这不是 Laravel 独有的,而是一种广泛使用的架构模式。
中间件简介
中间件的概念是,有一系列层包裹在您的应用程序周围,就像一个多层蛋糕或洋葱[¹]一样。正如图 10-1 所示,每个请求在进入应用程序时都会经过每个中间件层,然后生成的响应在发送给最终用户之前也会经过中间件层。
中间件通常被视为与应用程序逻辑分离的部分,并且通常设计为理论上适用于任何应用程序,而不仅限于您目前正在开发的应用程序。
中间件可以检查请求并根据其内容装饰或拒绝它。这意味着中间件非常适合像速率限制这样的用例:它们可以检查 IP 地址,查看在最后一分钟内访问此资源的次数,并在超过阈值时返回状态码 429(请求过多)。
因为中间件也可以在应用程序发送响应时访问响应,所以非常适合装饰响应。例如,Laravel 使用中间件将给定请求/响应周期中排队的所有 cookie 添加到响应中,然后再发送给最终用户。
但中间件最强大的用途之一来自于它们几乎可以是请求/响应周期中第一和最后的交互对象。这使得中间件非常适合像启用会话这样的功能——PHP 需要您尽早打开会话并在很晚时候关闭会话,而中间件也非常适合这种用途。
创建自定义中间件
假设我们希望有一个中间件,它拒绝使用DELETE HTTP 方法的每个请求,并在每个请求返回时发送一个 cookie。
有一个 Artisan 命令用于创建自定义中间件。让我们试试看:
php artisan make:middleware BanDeleteMethod
您现在可以打开app/Http/Middleware/BanDeleteMethod.php文件。默认内容如示例 10-17 所示。
示例 10-17. 默认中间件内容
...
class BanDeleteMethod
{
public function handle($request, Closure $next)
{
return $next($request);
}
}
这个 handle() 方法如何表示处理传入请求 和 传出响应是最难理解的中间件方面,所以让我们逐步来看一下。
理解中间件的 handle() 方法
首先,要记住中间件是层层叠加的,最后叠加在应用程序之上。注册的第一个中间件在请求进入时最先访问,然后请求依次传递给每个其他中间件,然后到达应用程序。然后通过中间件传递生成的响应,最后第一个中间件在响应输出时再次访问。
假设我们已将 BanDeleteMethod 注册为第一个运行的中间件。这意味着进入它的 $request 是原始请求,没有任何其他中间件的篡改。现在呢?
将该请求传递给 $next() 意味着将其传递给其余的中间件。$next() 闭包只是将该 $request 传递给堆栈中下一个中间件的 handle() 方法。然后它会一直传递到没有更多中间件可传递时,并最终到达应用程序。
接下来,响应是如何出来的?这可能比较难理解。应用程序返回一个响应,它通过中间件链返回上来——因为每个中间件都返回它的响应。因此,在同一个 handle() 方法中,中间件可以修饰 $request 并将其传递给 $next() 闭包,然后可以选择在最终将该输出返回给最终用户之前对接收到的输出做一些处理。让我们看一些伪代码来澄清这一点(参见 Example 10-18)。
示例 10-18. 解释中间件调用过程的伪代码
...
class BanDeleteMethod
{
public function handle($request, Closure $next)
{
// At this point, $request is the raw request from the user.
// Let's do something with it, just for fun.
if ($request->ip() === '192.168.1.1') {
return response('BANNED IP ADDRESS!', 403);
}
// Now we've decided to accept it. Let's pass it on to the next
// middleware in the stack. We pass it to $next(), and what is
// returned is the response after the $request has been passed
// down the stack of middleware to the application and the
// application's response has been passed back up the stack.
$response = $next($request);
// At this point, we can once again interact with the response
// just before it is returned to the user
$response->cookie('visited-our-site', true);
// Finally, we can release this response to the end user
return $response;
}
}
最后,让我们确保中间件实现我们实际承诺的功能(参见 Example 10-19)。
示例 10-19. 禁止 DELETE 方法的示例中间件
...
class BanDeleteMethod
{
public function handle($request, Closure $next)
{
// Test for the DELETE method
if ($request->method() === 'DELETE') {
return response(
"Get out of here with that delete method",
405
);
}
$response = $next($request);
// Assign cookie
$response->cookie('visited-our-site', true);
// Return response
return $response;
}
}
绑定中间件
我们还没有完成。我们需要以两种方式之一注册此中间件:全局注册或特定路由注册。
全局中间件适用于每个路由;路由中间件则逐个路由应用。
绑定全局中间件
这两种绑定都发生在 app/Http/Kernel.php 中。要将中间件作为全局添加,只需将其类名添加到 $middleware 属性中,如 Example 10-20 所示。
示例 10-20. 绑定全局中间件
// app/Http/Kernel.php
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\BanDeleteMethod::class,
];
绑定路由中间件
用于特定路由的中间件可以作为路由中间件或作为中间件组的一部分添加。让我们从前者开始。
路由中间件被添加到 app/Http/Kernel.php 的 $middlewareAliases 数组中。这类似于将它们添加到 $middleware,但我们必须为每一个中间件指定一个键,当将此中间件应用于特定路由时使用,正如在 Example 10-21 中所见。
示例 10-21. 绑定路由中间件
// app/Http/Kernel.php
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
...
'ban-delete' => \App\Http\Middleware\BanDeleteMethod::class,
];
现在我们可以在路由定义中使用这个中间件,就像在 Example 10-22 中所示。
示例 10-22. 在路由定义中应用路由中间件
// Doesn't make much sense for our current example...
Route::get('contacts', [ContactController::class, 'index'])->middleware('ban-delete');
// Makes more sense for our current example...
Route::prefix('api')->middleware('ban-delete')->group(function () {
// All routes related to an API
});
使用中间件组
中间件组本质上是在特定上下文中一起合理存在的预打包中间件束。
路由文件中的中间件组
routes/web.php 中的每个路由都位于web中间件组中。 routes/web.php 文件专门用于 web 路由,而routes/api.php 文件则用于 API 路由。如果您想要在其他组中添加路由,请继续阅读。
开箱即用,有两个组:web和api。 web组包含几乎每个 Laravel 页面请求都有用的所有中间件,包括用于 cookies、sessions 和 CSRF 保护的中间件。 api组则没有这些——它包含一个节流中间件和一个路由模型绑定中间件,就这些。这些都在app/Http/Kernel.php中定义。
您可以像向路由应用路由中间件一样,使用middleware()流畅方法向路由应用中间件组:
use App\Http\Controllers\HomeController;
Route::get('/', [HomeController::class, 'index']);
您还可以创建自己的中间件组,并向预定义的中间件组添加和移除路由中间件。它的工作方式与通常添加路由中间件相同,但您是将它们添加到$middlewareGroups数组中的键组中。
您可能会想知道这些中间件组与两个默认路由文件的对应关系。毫不奇怪,routes/web.php 文件使用web中间件组包裹,而routes/api.php 文件使用api中间件组包裹。
routes/* 文件在RouteServiceProvider中加载。看一下那里的map()方法(示例 10-23),您会发现mapWebRoutes()方法和mapApiRoutes()方法,每个方法都已将其各自的文件加载并已包裹在适当的中间件组中。
示例 10-23. 默认路由服务提供者
// App\Providers\RouteServiceProvider
public const HOME = '/home';
// protected $namespace = 'App\\Http\\Controllers';
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by(optional($request->user())->id ?: $request->ip());
});
}
如你所见,我们在示例 10-23 中使用路由器加载了一个路由组,其中包括web中间件组和api中间件组下的另一个路由组。
将参数传递给中间件
尽管不常见,但有时您需要向路由中间件传递参数。例如,您可能有一个身份验证中间件,根据您是保护member用户类型还是owner用户类型而采取不同的操作:
Route::get('company', function () {
return view('company.admin');
})->middleware('auth:owner');
要使此工作正常,您需要向中间件的handle()方法添加一个或多个参数,并相应地更新该方法的逻辑,如示例 10-24 所示。
示例 10-24. 定义接受参数的路由中间件
public function handle(Request $request, Closure $next, $role): Response
{
if (auth()->check() && auth()->user()->hasRole($role)) {
return $next($request);
}
return redirect('login');
}
请注意,您还可以向handle()方法添加多个参数,并通过用逗号分隔它们将多个参数传递给路由定义:
Route::get('company', function () {
return view('company.admin');
})->middleware('auth:owner,view');
默认中间件
Laravel 默认提供了相当多的中间件。让我们一起来看看每个中间件。
维护模式
我们经常需要临时将应用程序下线以执行某种形式的维护。Laravel 提供了名为“维护模式”的功能,并有一个中间件在每个响应中检查应用程序是否处于该模式下。
您可以使用 down Artisan 命令为您的应用程序启用维护模式:
php artisan down --refresh=5 --retry=30 --secret="long-password"
refresh
发送一个带有响应的头部,以指定秒数后刷新浏览器。
retry
设置 Retry-After 头部,带有指定的秒数。浏览器通常会忽略此头部。
secret
设置一个密码,允许某些用户绕过维护模式。要绕过维护模式,请导航到您的应用程序 URL,后面跟着您设置的秘密(例如 app.url/long-password)。这将重定向您到 / 应用程序 URL,并在您的浏览器上设置一个绕过 cookie,允许您在应用程序处于维护模式时正常访问。
要禁用维护模式,请使用 up Artisan 命令:
php artisan up
速率限制
如果您需要限制用户在特定时间内只能访问某些路由的次数(称为速率限制,在 API 中最常见),那么 Laravel 提供了一个即用即有的中间件:throttle。示例 10-25 展示了它的使用,使用 Laravel 提供的“api” RateLimiter 预设。
示例 10-25. 将速率限制中间件应用于路由
Route::middleware(['auth:api', 'throttle:api'])->group(function () {
Route::get('/profile', function () {
//
});
});
您可以定义尽可能多的自定义 RateLimiter 配置,查看 RouteServiceProvider 的 configureRateLimiting() 方法以获取默认的 api 配置,也可以创建您自己的配置。
正如您在 示例 10-26 中看到的,默认的 api 配置限制每分钟请求 60 次,分段为经过身份验证的 ID 或(如果用户未登录)IP 地址。
示例 10-26. 默认速率限制器定义
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
您还可以自定义当速率限制达到时发送的响应,根据用户或应用程序或请求条件指定不同的速率限制,甚至指定一个依次应用的速率限制器堆栈。查看速率限制文档以获取更多信息。
受信任的代理
如果您使用任何 Laravel 工具在应用程序内生成 URL,您会注意到 Laravel 检测当前请求是否通过 HTTP 或 HTTPS,并使用适当的协议生成链接。
然而,当您的应用程序前面有代理(例如负载均衡器或其他基于 Web 的代理)时,这并不总是有效。许多代理会发送非标准的头部,如 X_FORWARDED_PORT 和 X_FORWARDED_PROTO 到您的应用程序,并希望您的应用程序“信任”这些头部,解释它们,并将它们作为解释 HTTP 请求的一部分使用。为了使 Laravel 正确地将代理的 HTTPS 调用视为安全调用,并且为了让 Laravel 处理来自代理请求的其他头部,您需要定义它应该如何处理。
您可能不希望允许任何代理发送流量到您的应用程序;相反,您希望将您的应用程序锁定为仅信任特定代理,并且即使从这些代理中,您可能也只想信任某些转发头部。
Laravel 包含了TrustedProxy 包,它使您能够将某些流量源标记为“可信”,并标记您希望从这些源信任的转发头,并指定如何将它们映射到普通头部。
要配置您的应用程序将信任哪些代理,您可以编辑 App\Http\Middleware\TrustProxies 中间件,并将负载均衡器或代理的 IP 地址添加到 $proxies 数组中,如示例 10-27 所示。
示例 10-27. 配置 TrustProxies 中间件
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
如您所见,$headers 数组默认信任来自可信代理的所有转发头部;如果您想自定义此列表,请查看Symfony 关于信任代理的文档。
CORS
希望您从未遇到 CORS(跨源资源共享)的问题。这是我们希望始终正常运行的事情之一,当它不起作用时,会令人痛苦。
Laravel 的内置 CORS 中间件默认运行,并可以在config/cors.php中配置。它的默认配置对大多数应用程序都是合理的,但在其配置文件中,您可以排除 CORS 保护的路由,修改它操作的 HTTP 方法,并配置它如何与 CORS 头部交互。
测试
除了您作为开发人员在自己的测试中使用请求、响应和中间件的上下文之外,Laravel 本身实际上也大量使用它们。
当您像$this->get('/')这样使用应用程序测试调用时,您正在指示 Laravel 的应用程序测试框架生成代表您所描述交互的请求对象。然后,这些请求对象被传递给您的应用程序,就好像它们是实际访问一样。这就是为什么应用程序测试如此准确:您的应用程序实际上并不“知道”它正在与一个真实用户交互。
在这种情况下,您进行的许多断言—比如assertResponseOk()—都是针对应用程序测试框架生成的响应对象的断言。assertResponseOk() 方法只是查看响应对象,并断言其isOk() 方法返回true—这只是检查其状态码是否为 200。最终,在应用程序测试中,一切都像这是一个真实的页面请求一样运作。
发现自己需要在测试中使用一个请求的上下文?您可以随时从容器中获取一个,使用 $request = request()。或者您可以自己创建一个—Request 类的构造函数参数,所有参数都是可选的,如下所示:
$request = new Illuminate\Http\Request(
$query, // GET array
$request, // POST array
$attributes, // "attributes" array; empty is fine
$cookies, // Cookies array
$files, // Files array
$server, // Servers array
$content // Raw body data
);
如果您真的对一个例子感兴趣,请查看 Symfony 用于从 PHP 提供的全局变量创建新 Request 的方法:Symfony\Component\HttpFoundation\Request@createFromGlobals()。
Response 对象如果需要手动创建,甚至更加简单。以下是(可选的)参数:
$response = new Illuminate\Http\Response(
$content, // response content
$status, // HTTP status, default 200
$headers // array headers array
);
最后,如果在应用程序测试期间需要禁用中间件,请在该测试中导入 WithoutMiddleware 特性。你还可以使用 $this->withoutMiddleware() 方法仅在单个测试方法中禁用中间件。
TL;DR
每个进入 Laravel 应用程序的请求都会转换为 Illuminate Request 对象,然后通过所有中间件并由应用程序处理。应用程序生成一个 Response 对象,然后通过所有中间件(以相反的顺序)返回给最终用户。
Request 和 Response 对象负责封装和表示关于传入用户请求和传出服务器响应的每一个相关信息。
服务提供者汇集了绑定和注册类以供应用程序使用的相关行为。
中间件包裹应用程序,可以拒绝或装饰任何请求和响应。
¹ 或者一个 ogre。