认证

简介

许多 Web 应用都需要为用户提供认证和「登录」功能。在 Web 应用中实现这一功能可能是一项复杂且有潜在风险的工作。因此,Laravel 致力于为你提供快速、安全、便捷地实现认证所需的工具。

Laravel 的认证设施由「guards」和「providers」组成。Guard 定义了如何对每个请求的用户进行认证。例如,Laravel 内置了一个 session guard,它通过 session 存储和 cookie 来维持状态。

Provider 定义了如何从持久化存储中获取用户。Laravel 内置了对使用 Eloquent 和数据库查询构造器获取用户的支持。不过,你可以根据需要自由定义额外的 provider。

你的应用认证配置文件位于 config/auth.php。该文件包含多个文档完善的选项,用于调整 Laravel 认证服务的行为。

信息

Guard 和 provider 不应与「角色」和「权限」混淆。要了解如何通过权限授权用户操作,请参阅授权文档。

典型使用场景:

  • 用户登录与注册:通过起步套件快速搭建完整的登录、注册页面,包括密码重置和邮箱验证流程
  • API Token 认证:为移动端或第三方应用提供 API 令牌认证,使用 Sanctum 管理令牌的发放与撤销
  • 多端登录管理:用户修改密码后,使其他设备的会话失效,同时保持当前设备登录状态
  • 敏感操作确认:在执行修改密码、删除账户等敏感操作前,要求用户再次输入密码进行确认
  • 自定义认证体系:基于 JWT、OAuth 等协议构建自定义 Guard,对接外部认证服务
  • 无状态 API 认证:使用 HTTP Basic 认证或 API Token 为无状态的 API 接口提供身份验证

起步套件

想快速开始?在一个全新的 Laravel 应用中安装 Laravel 应用起步套件。迁移数据库后,在浏览器中访问 /register 或应用中分配的其他 URL。起步套件将负责为你搭建整个认证系统!

即使你最终选择不在 Laravel 应用中使用起步套件,安装起步套件也是学习如何在实际 Laravel 项目中实现 Laravel 全部认证功能的绝佳机会。 由于 Laravel 起步套件包含了认证控制器、路由和视图,你可以检查这些文件中的代码,了解 Laravel 的认证功能是如何实现的。

数据库注意事项

默认情况下,Laravel 在 app/Models 目录中包含了一个 App\Models\User Eloquent 模型。该模型可与默认的 Eloquent 认证驱动一起使用。

如果你的应用不使用 Eloquent,你可以使用 database 认证 provider,它使用 Laravel 查询构造器。如果你的应用使用 MongoDB,请查阅 MongoDB 官方的 Laravel 用户认证文档。

在为 App\Models\User 模型构建数据库表结构时,请确保密码列至少 60 个字符长。当然,新 Laravel 应用中包含的 users 表迁移文件已经创建了超过此长度的列。

此外,你应该验证 users(或等效)表包含一个 100 字符的可空字符串 remember_token 列。此列将用于存储用户登录时选择「记住我」选项的令牌。同样,新 Laravel 应用中包含的默认 users 表迁移文件已经包含此列。

生态系统概览

Laravel 提供了多个与认证相关的包。在继续之前,我们将回顾 Laravel 中的通用认证生态系统,并讨论每个包的用途。

首先,考虑认证是如何工作的。当使用 Web 浏览器时,用户将通过登录表单提供用户名和密码。如果凭据正确,应用会将已认证用户的信息存储在用户的 session 中。发送给浏览器的 cookie 包含 session ID,以便对应用的后续请求能将用户与正确的 session 关联。收到 session cookie 后,应用将根据 session ID 获取 session 数据,注意到认证信息已存储在 session 中,并将用户视为「已认证」。

当远程服务需要认证以访问 API 时,通常不使用 cookie 进行认证,因为没有 Web 浏览器。相反,远程服务在每次请求时向 API 发送 API 令牌。应用可以根据有效 API 令牌表验证传入的令牌,并将请求「认证」为由该 API 令牌关联的用户执行。

Laravel 内置浏览器认证服务

Laravel 包含内置的认证和 session 服务,通常通过 Auth 和 Session 门面访问。这些功能为从 Web 浏览器发起的请求提供基于 cookie 的认证。它们提供了验证用户凭据和认证用户的方法。此外,这些服务会自动将正确的认证数据存储在用户的 session 中,并向用户发放 session cookie。本文档包含了如何使用这些服务的讨论。

应用起步套件

如本文档所述,你可以手动与这些认证服务交互来构建应用自己的认证层。然而,为了帮助你更快上手,我们发布了免费起步套件,为整个认证层提供了健壮、现代的脚手架。

Laravel API 认证服务

Laravel 提供了两个可选包来帮助你管理 API 令牌和认证使用 API 令牌发起的请求:Passport 和 Sanctum。请注意,这些库与 Laravel 内置的基于 cookie 的认证库并不互斥。这些库主要关注 API 令牌认证,而内置认证服务关注基于 cookie 的浏览器认证。许多应用会同时使用 Laravel 内置的基于 cookie 的认证服务和 Laravel 的某个 API 认证包。

Passport

Passport 是一个 OAuth2 认证提供者,提供多种 OAuth2「授权类型」,允许你发放各种类型的令牌。一般来说,这是一个用于 API 认证的健壮且复杂的包。然而,大多数应用并不需要 OAuth2 规范提供的复杂功能,这可能对用户和开发者都造成困扰。此外,开发者历来对如何使用 Passport 等 OAuth2 认证提供者来认证 SPA 应用或移动应用感到困惑。

Sanctum

为了应对 OAuth2 的复杂性和开发者的困惑,我们着手构建一个更简单、更精简的认证包,能够同时处理来自 Web 浏览器的第一方 Web 请求和通过令牌的 API 请求。这一目标随着 Laravel Sanctum 的发布而实现。对于将提供第一方 Web UI 外加 API、由与后端 Laravel 应用分离的单页应用(SPA)驱动,或提供移动客户端的应用,Sanctum 应被视为首选和推荐的认证包。

Laravel Sanctum 是一个混合 Web/API 认证包,可以管理应用的整个认证流程。这是可能的,因为当基于 Sanctum 的应用收到请求时,Sanctum 会首先确定请求是否包含引用已认证 session 的 session cookie。Sanctum 通过调用我们之前讨论的 Laravel 内置认证服务来实现这一点。如果请求未通过 session cookie 认证,Sanctum 将检查请求中是否包含 API 令牌。如果存在 API 令牌,Sanctum 将使用该令牌认证请求。要了解有关此过程的更多信息,请查阅 Sanctum 的「工作原理」文档。

总结与技术选型

总之,如果你的应用将通过浏览器访问,并且你正在构建一个单体 Laravel 应用,你的应用将使用 Laravel 的内置认证服务。

其次,如果你的应用提供供第三方使用的 API,你将在 Passport 或 Sanctum 之间选择,为你的应用提供 API 令牌认证。一般而言,应尽可能优先使用 Sanctum,因为它是 API 认证、SPA 认证和移动认证的简单、完整解决方案,包括对「scopes」或「abilities」的支持。

如果你正在构建一个由 Laravel 后端驱动的单页应用(SPA),你应该使用 Laravel Sanctum。使用 Sanctum 时,你需要手动实现自己的后端认证路由,或使用 Laravel Fortify 作为无头认证后端服务,它为注册、密码重置、邮箱验证等功能提供路由和控制器。

当你的应用确实需要 OAuth2 规范提供的所有功能时,才选择 Passport。

最后,如果你想快速开始,我们很高兴地推荐我们的应用起步套件,作为快速启动新 Laravel 应用的方式,该应用已使用我们首选的认证技术栈——Laravel 内置认证服务。

认证快速入门

注意

本文档部分讨论通过 Laravel 应用起步套件认证用户,其中包含帮助你快速入门的 UI 脚手架。如果你想直接与 Laravel 认证系统集成,请查阅手动认证用户的文档。

安装起步套件

首先,你应该安装 Laravel 应用起步套件。我们的起步套件提供了设计精美的起点,帮助你在全新的 Laravel 应用中集成认证功能。

获取已认证用户

从起步套件创建应用并允许用户注册和认证后,你通常需要与当前已认证的用户进行交互。在处理传入请求时,你可以通过 Auth 门面的 user 方法访问已认证用户:

use Illuminate\Support\Facades\Auth;

// 获取当前已认证用户...
$user = Auth::user();

// 获取当前已认证用户的 ID...
$id = Auth::id();

或者,一旦用户通过认证,你可以通过 Illuminate\Http\Request 实例访问已认证用户。请记住,类型提示的类将自动注入到你的控制器方法中。通过类型提示 Illuminate\Http\Request 对象,你可以在应用的任何控制器方法中通过请求的 user 方法方便地访问已认证用户:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FlightController extends Controller
{
    /**
     * 更新现有航班信息。
     */
    public function update(Request $request): RedirectResponse
    {
        $user = $request->user();

        // ...

        return redirect('/flights');
    }
}

判断当前用户是否已认证

要判断发起传入 HTTP 请求的用户是否已认证,你可以使用 Auth 门面上的 check 方法。如果用户已认证,此方法将返回 true:

use Illuminate\Support\Facades\Auth;

if (Auth::check()) {
    // 用户已登录...
}

信息

虽然可以使用 check 方法判断用户是否已认证,但通常你会使用中间件来验证用户是否已认证,然后才允许用户访问某些路由/控制器。要了解更多信息,请查阅保护路由的文档。

保护路由

路由中间件可用于仅允许已认证用户访问给定路由。Laravel 内置了一个 auth 中间件,它是 Illuminate\Auth\Middleware\Authenticate 类的中间件别名。由于此中间件已由 Laravel 内部设置别名,你只需将中间件附加到路由定义即可:

Route::get('/flights', function () {
    // 仅已认证用户可访问此路由...
})->middleware('auth');

重定向未认证用户

当 auth 中间件检测到未认证用户时,它会将用户重定向到 login 命名路由。你可以在应用的 bootstrap/app.php 文件中使用 redirectGuestsTo 方法修改此行为:

use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->redirectGuestsTo('/login');

    // 使用闭包...
    $middleware->redirectGuestsTo(fn (Request $request) => route('login'));
})

重定向已认证用户

当 guest 中间件检测到已认证用户时,它会将用户重定向到 dashboard 或 home 命名路由。你可以在应用的 bootstrap/app.php 文件中使用 redirectUsersTo 方法修改此行为:

use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->redirectUsersTo('/panel');

    // 使用闭包...
    $middleware->redirectUsersTo(fn (Request $request) => route('panel'));
})

指定 Guard

将 auth 中间件附加到路由时,你还可以指定应使用哪个「guard」来认证用户。指定的 guard 应对应 auth.php 配置文件中 guards 数组的一个键:

Route::get('/flights', function () {
    // 仅已认证用户可访问此路由...
})->middleware('auth:admin');

登录限流

如果你使用的是我们的应用起步套件之一,登录尝试将自动应用速率限制。默认情况下,如果用户多次尝试后仍未能提供正确的凭据,将无法在一分钟内登录。限流针对用户的用户名/邮箱地址和 IP 地址唯一生效。

信息

如果你想对应用中的其他路由进行速率限制,请查阅速率限制文档。

手动认证用户

你并非必须使用 Laravel 应用起步套件中包含的认证脚手架。如果你选择不使用此脚手架,你需要直接使用 Laravel 认证类来管理用户认证。别担心,这很容易!

我们将通过 Auth 门面访问 Laravel 的认证服务,因此需要确保在类顶部导入 Auth 门面。接下来,让我们看看 attempt 方法。attempt 方法通常用于处理应用「登录」表单的认证尝试。如果认证成功,你应该重新生成用户的 session 以防止 session 固定攻击:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
     * 处理认证尝试。
     */
    public function authenticate(Request $request): RedirectResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();

            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ])->onlyInput('email');
    }
}

attempt 方法接受一个键/值对数组作为第一个参数。数组中的值将用于在数据库表中查找用户。因此,在上面的示例中,用户将通过 email 列的值来获取。如果找到用户,数据库中存储的哈希密码将与通过数组传递给方法的 password 值进行比较。你不应对传入请求的 password 值进行哈希处理,因为框架会在将其与数据库中的哈希密码比较之前自动哈希该值。如果两个哈希密码匹配,将为用户启动已认证的 session。

请记住,Laravel 的认证服务将根据认证 guard 的「provider」配置从数据库中获取用户。在默认的 config/auth.php 配置文件中,指定了 Eloquent 用户 provider,并指示在获取用户时使用 App\Models\User 模型。你可以根据应用需要在配置文件中更改这些值。

如果认证成功,attempt 方法将返回 true。否则,将返回 false。

Laravel 重定向器提供的 intended 方法会将用户重定向到他们被认证中间件拦截之前试图访问的 URL。如果预期目的地不可用,可以为此方法提供一个回退 URI。

指定额外条件

如果你愿意,除了用户的邮箱和密码之外,你还可以向认证查询添加额外的查询条件。为此,我们只需将查询条件添加到传递给 attempt 方法的数组中。例如,我们可以验证用户是否被标记为「活跃」:

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // 认证成功...
}

对于复杂的查询条件,你可以在凭据数组中提供一个闭包。此闭包将与查询实例一起调用,允许你根据应用需求自定义查询:

use Illuminate\Database\Eloquent\Builder;

if (Auth::attempt([
    'email' => $email,
    'password' => $password,
    fn (Builder $query) => $query->has('activeSubscription'),
])) {
    // 认证成功...
}

注意

在这些示例中,email 不是必选项,它仅用作示例。你应该使用数据库表中对应「用户名」的任何列名。

attemptWhen 方法接收一个闭包作为第二个参数,可用于在实际认证用户之前对潜在用户进行更全面的检查。闭包接收潜在用户,应返回 true 或 false 以指示是否可以认证该用户:

if (Auth::attemptWhen([
    'email' => $email,
    'password' => $password,
], function (User $user) {
    return $user->isNotBanned();
})) {
    // 认证成功...
}

访问特定 Guard 实例

通过 Auth 门面的 guard 方法,你可以指定在认证用户时要使用的 guard 实例。这允许你使用完全独立的可认证模型或用户表来管理应用不同部分的认证。

传递给 guard 方法的 guard 名称应对应 auth.php 配置文件中配置的 guard 之一:

if (Auth::guard('admin')->attempt($credentials)) {
    // ...
}

记住用户

许多 Web 应用在其登录表单上提供「记住我」复选框。如果你想在应用中提供「记住我」功能,可以将布尔值作为 attempt 方法的第二个参数传递。

当此值为 true 时,Laravel 将无限期地保持用户已认证状态,直到他们手动退出。你的 users 表必须包含字符串 remember_token 列,用于存储「记住我」令牌。新 Laravel 应用中包含的 users 表迁移已包含此列:

use Illuminate\Support\Facades\Auth;

if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
    // 用户已被记住...
}

如果你的应用提供「记住我」功能,你可以使用 viaRemember 方法来判断当前已认证用户是否通过「记住我」cookie 认证:

use Illuminate\Support\Facades\Auth;

if (Auth::viaRemember()) {
    // ...
}

其他认证方式

认证用户实例

如果你需要将现有用户实例设置为当前已认证用户,可以将用户实例传递给 Auth 门面的 login 方法。给定的用户实例必须是 Illuminate\Contracts\Auth\Authenticatable 契约的实现。Laravel 包含的 App\Models\User 模型已实现此接口。当你已有有效的用户实例时(例如用户刚注册后),这种认证方式非常有用:

use Illuminate\Support\Facades\Auth;

Auth::login($user);

你可以将布尔值作为 login 方法的第二个参数传递。此值指示已认证 session 是否需要「记住我」功能。请记住,这意味着 session 将无限期认证,直到用户手动退出应用:

Auth::login($user, $remember = true);

如果需要,可以在调用 login 方法之前指定认证 guard:

Auth::guard('admin')->login($user);

通过 ID 认证用户

要使用用户数据库记录的主键来认证用户,你可以使用 loginUsingId 方法。此方法接受你希望认证的用户的主键:

Auth::loginUsingId(1);

你可以向 loginUsingId 方法的 remember 参数传递布尔值。此值指示已认证 session 是否需要「记住我」功能。请记住,这意味着 session 将无限期认证,直到用户手动退出应用:

Auth::loginUsingId(1, remember: true);

一次性认证用户

你可以使用 once 方法为单个请求认证用户。调用此方法时不会使用 session 或 cookie,且不会分发 Login 事件:

if (Auth::once($credentials)) {
    // ...
}

HTTP 基础认证

HTTP 基础认证提供了一种快捷方式来认证应用用户,无需设置专门的「登录」页面。要开始使用,将 auth.basic 中间件附加到路由。auth.basic 中间件已包含在 Laravel 框架中,因此你无需定义它:

Route::get('/profile', function () {
    // 仅已认证用户可访问此路由...
})->middleware('auth.basic');

一旦中间件附加到路由,在浏览器中访问该路由时将自动提示输入凭据。默认情况下,auth.basic 中间件会假设 users 数据库表上的 email 列是用户的「用户名」。

关于 FastCGI 的说明

如果你使用 PHP FastCGI 和 Apache 来服务你的 Laravel 应用,HTTP 基础认证可能无法正常工作。要纠正这些问题,可以在应用的 .htaccess 文件中添加以下行:

RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

无状态 HTTP 基础认证

你还可以使用 HTTP 基础认证而不在 session 中设置用户标识 cookie。这在你选择使用 HTTP 认证来认证应用 API 请求时主要有用。为此,定义一个中间件来调用 onceBasic 方法。如果 onceBasic 方法未返回响应,请求可以继续传递到应用中:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class AuthenticateOnceWithBasicAuth
{
    /**
     * 处理传入请求。
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return Auth::onceBasic() ?: $next($request);
    }

}

接下来,将中间件附加到路由:

Route::get('/api/user', function () {
    // 仅已认证用户可访问此路由...
})->middleware(AuthenticateOnceWithBasicAuth::class);

退出登录

要手动将用户退出应用,你可以使用 Auth 门面提供的 logout 方法。这将从用户的 session 中移除认证信息,以便后续请求不再被认证。

除了调用 logout 方法外,建议你使用户的 session 失效并重新生成其 CSRF 令牌。将用户退出后,你通常会将用户重定向到应用的根路径:

use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;

/**
 * 将用户退出应用。
 */
public function logout(Request $request): RedirectResponse
{
    Auth::logout();

    $request->session()->invalidate();

    $request->session()->regenerateToken();

    return redirect('/');
}

使其他设备的会话失效

Laravel 还提供了一种机制来使在其他设备上活跃的用户 session 失效并「退出」,同时不使当前设备的 session 失效。此功能通常在用户更改或更新密码时使用,你希望使其他设备的 session 失效,同时保持当前设备已认证。

在开始之前,你应该确保 Illuminate\Session\Middleware\AuthenticateSession 中间件包含在应接收 session 认证的路由上。通常,你应该将此中间件放在路由组定义中,以便它可以应用到应用的大多数路由。默认情况下,AuthenticateSession 中间件可以通过 auth.session 中间件别名附加到路由:

Route::middleware(['auth', 'auth.session'])->group(function () {
    Route::get('/', function () {
        // ...
    });
});

然后,你可以使用 Auth 门面提供的 logoutOtherDevices 方法。此方法要求用户确认其当前密码,你的应用应通过输入表单接受该密码:

use Illuminate\Support\Facades\Auth;

Auth::logoutOtherDevices($currentPassword);

当调用 logoutOtherDevices 方法时,用户的其他 session 将完全失效,这意味着他们将从未前认证过的所有 guard 中「退出」。

密码确认

在构建应用时,你可能偶尔会有一些操作需要用户在执行操作前或被重定向到应用的敏感区域前确认其密码。Laravel 包含内置中间件使此过程变得轻而易举。实现此功能需要你定义两个路由:一个用于显示要求用户确认密码的视图,另一个用于确认密码有效并将用户重定向到其预期目的地。

信息

以下文档讨论如何直接与 Laravel 的密码确认功能集成;然而,如果你想更快上手,Laravel 应用起步套件已包含对此功能的支持!

配置

用户确认密码后,将在三小时内不再被要求确认密码。但是,你可以通过更改应用 config/auth.php 配置文件中 password_timeout 配置值来配置用户被再次要求输入密码之前的时间长度。

路由

密码确认表单

首先,我们将定义一个路由来显示要求用户确认密码的视图:

Route::get('/confirm-password', function () {
    return view('auth.confirm-password');
})->middleware('auth')->name('password.confirm');

如你所料,此路由返回的视图应包含一个带有 password 字段的表单。此外,可以随意在视图中包含文本,说明用户正在进入应用的受保护区域,必须确认其密码。

确认密码

接下来,我们将定义一个路由来处理来自「确认密码」视图的表单请求。此路由将负责验证密码并将用户重定向到其预期目的地:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

Route::post('/confirm-password', function (Request $request) {
    if (! Hash::check($request->password, $request->user()->password)) {
        return back()->withErrors([
            'password' => ['The provided password does not match our records.']
        ]);
    }

    $request->session()->passwordConfirmed();

    return redirect()->intended();
})->middleware(['auth', 'throttle:6,1']);

在继续之前,让我们更详细地检查此路由。首先,确定请求的 password 字段是否确实与已认证用户的密码匹配。如果密码有效,我们需要通知 Laravel 的 session 用户已确认其密码。passwordConfirmed 方法将在用户的 session 中设置一个时间戳,Laravel 可用它来确定用户上次确认密码的时间。最后,我们可以将用户重定向到其预期目的地。

保护路由

你应该确保任何执行需要最近密码确认的操作的路由都分配了 password.confirm 中间件。此中间件包含在 Laravel 的默认安装中,会自动将用户的预期目的地存储在 session 中,以便用户在确认密码后可以重定向到该位置。将用户的预期目的地存储在 session 中后,中间件会将用户重定向到 password.confirm 命名路由:

Route::get('/settings', function () {
    // ...
})->middleware(['password.confirm']);

Route::post('/settings', function () {
    // ...
})->middleware(['password.confirm']);

添加自定义 Guard

你可以使用 Auth 门面上的 extend 方法定义自己的认证 guard。你应该在服务提供者中调用 extend 方法。由于 Laravel 已内置 AppServiceProvider,我们可以将代码放在该 provider 中:

<?php

namespace App\Providers;

use App\Services\Auth\JwtGuard;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    // ...

    /**
     * 引导任何应用服务。
     */
    public function boot(): void
    {
        Auth::extend('jwt', function (Application $app, string $name, array $config) {
            // 返回 Illuminate\Contracts\Auth\Guard 的实例...

            return new JwtGuard(Auth::createUserProvider($config['provider']));
        });
    }
}

如上例所示,传递给 extend 方法的回调应返回 Illuminate\Contracts\Auth\Guard 的实现。此接口包含一些你需要实现以定义自定义 guard 的方法。定义自定义 guard 后,你可以在 auth.php 配置文件的 guards 配置中引用该 guard:

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

闭包请求 Guard

实现自定义的、基于 HTTP 请求的认证系统最简单的方式是使用 Auth::viaRequest 方法。此方法允许你使用单个闭包快速定义认证流程。

要开始使用,在应用 AppServiceProvider 的 boot 方法中调用 Auth::viaRequest 方法。viaRequest 方法接受一个认证驱动名称作为第一个参数。此名称可以是描述你的自定义 guard 的任何字符串。传递给方法的第二个参数应是一个闭包,该闭包接收传入 HTTP 请求并返回用户实例,如果认证失败则返回 null:

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * 引导任何应用服务。
 */
public function boot(): void
{
    Auth::viaRequest('custom-token', function (Request $request) {
        return User::where('token', (string) $request->token)->first();
    });
}

定义自定义认证驱动后,你可以在 auth.php 配置文件的 guards 配置中将其配置为驱动:

'guards' => [
    'api' => [
        'driver' => 'custom-token',
    ],
],

最后,在将认证中间件分配给路由时,你可以引用该 guard:

Route::middleware('auth:api')->group(function () {
    // ...
});

添加自定义用户提供者

如果你不使用传统的关系型数据库来存储用户,你将需要使用自己的认证用户提供者扩展 Laravel。我们将使用 Auth 门面上的 provider 方法定义自定义用户提供者。用户提供者解析器应返回 Illuminate\Contracts\Auth\UserProvider 的实现:

<?php

namespace App\Providers;

use App\Extensions\MongoUserProvider;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    // ...

    /**
     * 引导任何应用服务。
     */
    public function boot(): void
    {
        Auth::provider('mongo', function (Application $app, array $config) {
            // 返回 Illuminate\Contracts\Auth\UserProvider 的实例...

            return new MongoUserProvider($app->make('mongo.connection'));
        });
    }
}

使用 provider 方法注册提供者后,你可以在 auth.php 配置文件中切换到新的用户提供者。首先,定义一个使用新驱动的 provider:

'providers' => [
    'users' => [
        'driver' => 'mongo',
    ],
],

最后,你可以在 guards 配置中引用此提供者:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
],

用户提供者契约

Illuminate\Contracts\Auth\UserProvider 实现负责从持久化存储系统(如 MySQL、MongoDB 等)中获取 Illuminate\Contracts\Auth\Authenticatable 实现。这两个接口允许 Laravel 认证机制持续运作,无论用户数据如何存储或使用什么类型的类来表示已认证用户:

让我们看看 Illuminate\Contracts\Auth\UserProvider 契约:

<?php

namespace Illuminate\Contracts\Auth;

interface UserProvider
{
    public function retrieveById($identifier);
    public function retrieveByToken($identifier, $token);
    public function updateRememberToken(Authenticatable $user, $token);
    public function retrieveByCredentials(array $credentials);
    public function validateCredentials(Authenticatable $user, array $credentials);
    public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false);
}

retrieveById 函数通常接收一个代表用户的键,例如 MySQL 数据库的自增 ID。匹配该 ID 的 Authenticatable 实现应被获取并由该方法返回。

retrieveByToken 函数通过用户的唯一 $identifier 和「记住我」$token 获取用户,通常存储在 remember_token 等数据库列中。与上一个方法一样,具有匹配令牌值的 Authenticatable 实现应由此方法返回。

updateRememberToken 方法用新的 $token 更新 $user 实例的 remember_token。在成功的「记住我」认证尝试或用户退出登录时,会为用户分配新的令牌。

retrieveByCredentials 方法接收尝试与应用认证时传递给 Auth::attempt 方法的凭据数组。然后,该方法应「查询」底层持久化存储以查找匹配这些凭据的用户。通常,此方法将运行一个带有「where」条件的查询,搜索具有匹配 $credentials['username'] 值的用户记录。该方法应返回 Authenticatable 的实现。此方法不应尝试进行任何密码验证或认证。

validateCredentials 方法应将给定的 $user 与 $credentials 进行比较以认证用户。例如,此方法通常使用 Hash::check 方法将 $user->getAuthPassword() 的值与 $credentials['password'] 的值进行比较。此方法应返回 true 或 false,指示密码是否有效。

rehashPasswordIfRequired 方法应在需要且支持时重新哈希给定 $user 的密码。例如,此方法通常使用 Hash::needsRehash 方法来确定 $credentials['password'] 值是否需要重新哈希。如果密码需要重新哈希,该方法应使用 Hash::make 方法重新哈希密码并更新底层持久化存储中的用户记录。

可认证契约

现在我们已经探索了 UserProvider 上的每个方法,让我们看看 Authenticatable 契约。请记住,用户提供者应从 retrieveById、retrieveByToken 和 retrieveByCredentials 方法返回此接口的实现:

<?php

namespace Illuminate\Contracts\Auth;

interface Authenticatable
{
    public function getAuthIdentifierName();
    public function getAuthIdentifier();
    public function getAuthPasswordName();
    public function getAuthPassword();
    public function getRememberToken();
    public function setRememberToken($value);
    public function getRememberTokenName();
}

此接口很简单。getAuthIdentifierName 方法应返回用户的「主键」列名,getAuthIdentifier 方法应返回用户的「主键」。使用 MySQL 后端时,这可能是分配给用户记录的自增主键。getAuthPasswordName 方法应返回用户密码列的名称。getAuthPassword 方法应返回用户的哈希密码。

此接口允许认证系统与任何「用户」类一起工作,无论你使用什么 ORM 或存储抽象层。默认情况下,Laravel 在 app/Models 目录中包含了一个实现此接口的 App\Models\User 类。

自动密码重哈希

Laravel 的默认密码哈希算法是 bcrypt。bcrypt 哈希的「工作因子」可以通过应用的 config/hashing.php 配置文件或 BCRYPT_ROUNDS 环境变量进行调整。

通常,随着 CPU/GPU 处理能力的提升,bcrypt 工作因子应随时间推移而增加。如果你增加了应用的 bcrypt 工作因子,当用户通过 Laravel 起步套件认证或通过 attempt 方法手动认证用户时,Laravel 将优雅地自动重新哈希用户密码。

通常,自动密码重哈希不会影响你的应用;但是,你可以通过发布 hashing 配置文件来禁用此行为:

php artisan config:publish hashing

发布配置文件后,你可以将 rehash_on_login 配置值设置为 false:

'rehash_on_login' => false,

事件

Laravel 在认证过程中会分发多种事件。你可以为以下任何事件定义监听器:

事件名称
Illuminate\Auth\Events\Registered
Illuminate\Auth\Events\Attempting
Illuminate\Auth\Events\Authenticated
Illuminate\Auth\Events\Login
Illuminate\Auth\Events\Failed
Illuminate\Auth\Events\Validated
Illuminate\Auth\Events\Verified
Illuminate\Auth\Events\Logout
Illuminate\Auth\Events\CurrentDeviceLogout
Illuminate\Auth\Events\OtherDeviceLogout
Illuminate\Auth\Events\Lockout
Illuminate\Auth\Events\PasswordReset
Illuminate\Auth\Events\PasswordResetLinkSent