Skip to content

Laravel Sanctum

介绍

Laravel Sanctum 为 SPA(单页应用)、移动应用和简单的基于令牌的 API 提供了一个轻量级的认证系统。Sanctum 允许应用的每个用户为其账户生成多个 API 令牌。这些令牌可以被授予能力/范围,以指定令牌被允许执行的操作。

工作原理

Laravel Sanctum 旨在解决两个独立的问题。在深入研究库之前,让我们先讨论每个问题。

API 令牌

首先,Sanctum 是一个简单的包,您可以使用它为用户颁发 API 令牌,而无需 OAuth 的复杂性。此功能的灵感来自 GitHub 和其他应用程序,它们颁发“个人访问令牌”。例如,想象一下应用程序的“账户设置”中有一个屏幕,用户可以为其账户生成 API 令牌。您可以使用 Sanctum 来生成和管理这些令牌。这些令牌通常具有很长的过期时间(几年),但用户可以随时手动撤销。

Laravel Sanctum 通过在单个数据库表中存储用户 API 令牌并通过 Authorization 头验证传入的 HTTP 请求来提供此功能,该头应包含有效的 API 令牌。

SPA 认证

其次,Sanctum 提供了一种简单的方法来认证需要与 Laravel 驱动的 API 通信的单页应用程序(SPA)。这些 SPA 可能存在于与 Laravel 应用程序相同的存储库中,也可能是一个完全独立的存储库,例如使用 Vue CLI 创建的 SPA 或 Next.js 应用程序。

对于此功能,Sanctum 不使用任何类型的令牌。相反,Sanctum 使用 Laravel 内置的基于 cookie 的会话认证服务。通常,Sanctum 利用 Laravel 的 web 认证守卫来实现这一点。这提供了 CSRF 保护、会话认证的好处,并防止通过 XSS 泄露认证凭据。

Sanctum 仅在传入请求来自您自己的 SPA 前端时尝试使用 cookie 进行认证。当 Sanctum 检查传入的 HTTP 请求时,它将首先检查认证 cookie,如果没有,则 Sanctum 将检查 Authorization 头中是否有有效的 API 令牌。

lightbulb

仅使用 Sanctum 进行 API 令牌认证或仅使用 SPA 认证是完全可以的。使用 Sanctum 并不意味着您必须使用它提供的两个功能。

安装

lightbulb

Laravel 的最新版本已经包含 Laravel Sanctum。但是,如果您的应用程序的 composer.json 文件不包含 laravel/sanctum,您可以按照以下安装说明进行操作。

您可以通过 Composer 包管理器安装 Laravel Sanctum:

shell
composer require laravel/sanctum

接下来,您应该使用 vendor:publish Artisan 命令发布 Sanctum 配置和迁移文件。sanctum 配置文件将被放置在应用程序的 config 目录中:

shell
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

最后,您应该运行数据库迁移。Sanctum 将创建一个数据库表来存储 API 令牌:

shell
php artisan migrate

接下来,如果您计划使用 Sanctum 来认证 SPA,您应该将 Sanctum 的中间件添加到应用程序的 app/Http/Kernel.php 文件中的 api 中间件组中:

php
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

迁移自定义

如果您不打算使用 Sanctum 的默认迁移,您应该在 App\Providers\AppServiceProvider 类的 register 方法中调用 Sanctum::ignoreMigrations 方法。您可以通过执行以下命令导出默认迁移:php artisan vendor:publish --tag=sanctum-migrations

配置

覆盖默认模型

虽然通常不需要,但您可以自由扩展 Sanctum 内部使用的 PersonalAccessToken 模型:

php
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
{
    // ...
}

然后,您可以通过 Sanctum 提供的 usePersonalAccessTokenModel 方法指示 Sanctum 使用自定义模型。通常,您应该在应用程序的某个服务提供者的 boot 方法中调用此方法:

php
use App\Models\Sanctum\PersonalAccessToken;
use Laravel\Sanctum\Sanctum;

/**
 * 启动任何应用程序服务。
 *
 * @return void
 */
public function boot()
{
    Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}

API 令牌认证

lightbulb

您不应使用 API 令牌来认证您自己的第一方 SPA。相反,使用 Sanctum 的内置 SPA 认证功能

颁发 API 令牌

Sanctum 允许您颁发 API 令牌/个人访问令牌,这些令牌可用于认证对应用程序的 API 请求。使用 API 令牌进行请求时,令牌应作为 Bearer 令牌包含在 Authorization 头中。

要开始为用户颁发令牌,您的用户模型应使用 Laravel\Sanctum\HasApiTokens trait:

php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

要颁发令牌,您可以使用 createToken 方法。createToken 方法返回一个 Laravel\Sanctum\NewAccessToken 实例。API 令牌在存储到数据库之前使用 SHA-256 哈希进行哈希处理,但您可以使用 NewAccessToken 实例的 plainTextToken 属性访问令牌的纯文本值。您应该在令牌创建后立即将此值显示给用户:

php
use Illuminate\Http\Request;

Route::post('/tokens/create', function (Request $request) {
    $token = $request->user()->createToken($request->token_name);

    return ['token' => $token->plainTextToken];
});

您可以使用 HasApiTokens trait 提供的 tokens Eloquent 关系访问用户的所有令牌:

php
foreach ($user->tokens as $token) {
    //
}

令牌能力

Sanctum 允许您为令牌分配“能力”。能力的作用类似于 OAuth 的“范围”。您可以将字符串能力数组作为第二个参数传递给 createToken 方法:

php
return $user->createToken('token-name', ['server:update'])->plainTextToken;

处理由 Sanctum 认证的传入请求时,您可以使用 tokenCan 方法确定令牌是否具有给定的能力:

php
if ($user->tokenCan('server:update')) {
    //
}

令牌能力中间件

Sanctum 还包括两个中间件,可用于验证传入请求是否使用具有给定能力的令牌进行认证。要开始,请将以下中间件添加到应用程序的 app/Http/Kernel.php 文件的 $routeMiddleware 属性中:

php
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,

可以将 abilities 中间件分配给路由,以验证传入请求的令牌是否具有所有列出的能力:

php
Route::get('/orders', function () {
    // 令牌具有“check-status”和“place-orders”能力...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);

可以将 ability 中间件分配给路由,以验证传入请求的令牌是否具有至少一个列出的能力:

php
Route::get('/orders', function () {
    // 令牌具有“check-status”或“place-orders”能力...
})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);

第一方 UI 发起的请求

为了方便起见,如果传入的认证请求来自您的第一方 SPA 并且您正在使用 Sanctum 的内置 SPA 认证,则 tokenCan 方法将始终返回 true

然而,这并不一定意味着您的应用程序必须允许用户执行该操作。通常,应用程序的 授权策略 将确定令牌是否被授予执行能力的权限,并检查用户实例本身是否应被允许执行该操作。

例如,如果我们想象一个管理服务器的应用程序,这可能意味着检查令牌是否被授权更新服务器并且服务器属于用户:

php
return $request->user()->id === $server->user_id &&
       $request->user()->tokenCan('server:update')

起初,允许 tokenCan 方法被调用并始终为第一方 UI 发起的请求返回 true 可能看起来很奇怪;然而,能够始终假设 API 令牌可用并可以通过 tokenCan 方法进行检查是很方便的。通过采取这种方法,您可以始终在应用程序的授权策略中调用 tokenCan 方法,而不必担心请求是由应用程序的 UI 触发的还是由 API 的第三方消费者发起的。

保护路由

要保护路由,以便所有传入请求都必须经过认证,您应该在 routes/web.phproutes/api.php 路由文件中将 sanctum 认证守卫附加到受保护的路由上。此守卫将确保传入请求经过认证,无论是状态化的 cookie 认证请求,还是如果请求来自第三方,则包含有效的 API 令牌头。

您可能想知道为什么我们建议您在应用程序的 routes/web.php 文件中使用 sanctum 守卫认证路由。请记住,Sanctum 将首先尝试使用 Laravel 的典型会话认证 cookie 认证传入请求。如果该 cookie 不存在,则 Sanctum 将尝试使用请求的 Authorization 头中的令牌认证请求。此外,使用 Sanctum 认证所有请求可确保我们始终可以在当前认证的用户实例上调用 tokenCan 方法:

php
use Illuminate\Http\Request;

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

撤销令牌

您可以通过使用 Laravel\Sanctum\HasApiTokens trait 提供的 tokens 关系从数据库中删除令牌来“撤销”令牌:

php
// 撤销所有令牌...
$user->tokens()->delete();

// 撤销用于认证当前请求的令牌...
$request->user()->currentAccessToken()->delete();

// 撤销特定令牌...
$user->tokens()->where('id', $tokenId)->delete();

令牌过期

默认情况下,Sanctum 令牌永不过期,只能通过 撤销令牌 使其无效。然而,如果您希望为应用程序的 API 令牌配置过期时间,可以通过应用程序的 sanctum 配置文件中定义的 expiration 配置选项来实现。此配置选项定义了颁发的令牌在多少分钟后将被视为过期:

php
'expiration' => 525600,

如果您为应用程序配置了令牌过期时间,您可能还希望 安排任务 来修剪应用程序的过期令牌。幸运的是,Sanctum 包含一个 sanctum:prune-expired Artisan 命令,您可以使用它来完成此任务。例如,您可以配置一个计划任务来每天删除所有已过期至少 24 小时的令牌数据库记录:

php
$schedule->command('sanctum:prune-expired --hours=24')->daily();

SPA 认证

Sanctum 还提供了一种简单的方法来认证需要与 Laravel 驱动的 API 通信的单页应用程序(SPA)。这些 SPA 可能存在于与 Laravel 应用程序相同的存储库中,也可能是一个完全独立的存储库。

对于此功能,Sanctum 不使用任何类型的令牌。相反,Sanctum 使用 Laravel 内置的基于 cookie 的会话认证服务。这种认证方法提供了 CSRF 保护、会话认证的好处,并防止通过 XSS 泄露认证凭据。

exclamation

为了进行认证,您的 SPA 和 API 必须共享相同的顶级域。然而,它们可以放置在不同的子域上。此外,您应该确保在请求中发送 Accept: application/json 头。

配置

配置您的第一方域

首先,您应该配置 SPA 将从哪些域发出请求。您可以使用 sanctum 配置文件中的 stateful 配置选项配置这些域。此配置设置确定哪些域在向 API 发出请求时将使用 Laravel 会话 cookie 进行“状态化”认证。

exclamation

如果您通过包含端口的 URL 访问应用程序(例如 127.0.0.1:8000),您应该确保在域中包含端口号。

Sanctum 中间件

接下来,您应该将 Sanctum 的中间件添加到应用程序的 app/Http/Kernel.php 文件中的 api 中间件组中。此中间件负责确保来自 SPA 的传入请求可以使用 Laravel 的会话 cookie 进行认证,同时仍然允许来自第三方或移动应用程序的请求使用 API 令牌进行认证:

php
'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

CORS 和 Cookies

如果您在从位于不同子域的 SPA 认证应用程序时遇到问题,您可能错误配置了 CORS(跨域资源共享)或会话 cookie 设置。

您应该确保应用程序的 CORS 配置返回 Access-Control-Allow-Credentials 头,其值为 True。这可以通过将应用程序的 config/cors.php 配置文件中的 supports_credentials 选项设置为 true 来实现。

此外,您应该在应用程序的全局 axios 实例上启用 withCredentials 选项。通常,这应该在 resources/js/bootstrap.js 文件中执行。如果您不使用 Axios 从前端发出 HTTP 请求,您应该在自己的 HTTP 客户端上执行等效配置:

js
axios.defaults.withCredentials = true;

最后,您应该确保应用程序的会话 cookie 域配置支持根域的任何子域。您可以通过在应用程序的 config/session.php 配置文件中为域名前缀添加一个 . 来实现:

php
'domain' => '.domain.com',

认证

CSRF 保护

要认证您的 SPA,SPA 的“登录”页面应首先向 /sanctum/csrf-cookie 端点发出请求,以初始化应用程序的 CSRF 保护:

js
axios.get('/sanctum/csrf-cookie').then(response => {
    // 登录...
});

在此请求期间,Laravel 将设置一个包含当前 CSRF 令牌的 XSRF-TOKEN cookie。然后,此令牌应在后续请求中通过 X-XSRF-TOKEN 头传递,一些 HTTP 客户端库(如 Axios 和 Angular HttpClient)将自动为您执行此操作。如果您的 JavaScript HTTP 库未为您设置该值,您将需要手动将 X-XSRF-TOKEN 头设置为与此路由设置的 XSRF-TOKEN cookie 的值匹配。

登录

一旦 CSRF 保护已初始化,您应该向 Laravel 应用程序的 /login 路由发出 POST 请求。此 /login 路由可以 手动实现 或使用无头认证包(如 Laravel Fortify)实现。

如果登录请求成功,您将被认证,并且对应用程序路由的后续请求将通过 Laravel 应用程序向客户端发出的会话 cookie 自动进行认证。此外,由于应用程序已经向 /sanctum/csrf-cookie 路由发出了请求,只要您的 JavaScript HTTP 客户端在 X-XSRF-TOKEN 头中发送 XSRF-TOKEN cookie 的值,后续请求应自动获得 CSRF 保护。

当然,如果用户的会话由于缺乏活动而过期,对 Laravel 应用程序的后续请求可能会收到 401 或 419 HTTP 错误响应。在这种情况下,您应该将用户重定向到 SPA 的登录页面。

exclamation

您可以自由编写自己的 /login 端点;然而,您应该确保它使用 Laravel 提供的标准 基于会话的认证服务 进行用户认证。通常,这意味着使用 web 认证守卫。

保护路由

要保护路由,以便所有传入请求都必须经过认证,您应该在 routes/api.php 文件中将 sanctum 认证守卫附加到 API 路由上。此守卫将确保传入请求经过认证,无论是来自 SPA 的状态化认证请求,还是如果请求来自第三方,则包含有效的 API 令牌头:

php
use Illuminate\Http\Request;

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

授权私有广播频道

如果您的 SPA 需要与 私有/存在广播频道 进行认证,您应该将 Broadcast::routes 方法调用放在 routes/api.php 文件中:

php
Broadcast::routes(['middleware' => ['auth:sanctum']]);

接下来,为了使 Pusher 的授权请求成功,您需要在初始化 Laravel Echo 时提供自定义 Pusher authorizer。这允许您的应用程序配置 Pusher 使用 正确配置的跨域请求axios 实例:

js
window.Echo = new Echo({
    broadcaster: "pusher",
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
    encrypted: true,
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    authorizer: (channel, options) => {
        return {
            authorize: (socketId, callback) => {
                axios.post('/api/broadcasting/auth', {
                    socket_id: socketId,
                    channel_name: channel.name
                })
                .then(response => {
                    callback(false, response.data);
                })
                .catch(error => {
                    callback(true, error);
                });
            }
        };
    },
})

移动应用认证

您还可以使用 Sanctum 令牌来认证移动应用程序对 API 的请求。认证移动应用程序请求的过程与认证第三方 API 请求类似;然而,在颁发 API 令牌时会有一些小的差异。

颁发 API 令牌

要开始,请创建一个接受用户电子邮件/用户名、密码和设备名称的路由,然后将这些凭据交换为新的 Sanctum 令牌。此端点提供的“设备名称”仅用于信息目的,可以是您希望的任何值。通常,设备名称值应为用户可以识别的名称,例如“努诺的 iPhone 12”。

通常,您将在移动应用程序的“登录”屏幕中向令牌端点发出请求。端点将返回纯文本 API 令牌,然后可以将其存储在移动设备上,并用于发出其他 API 请求:

php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Route::post('/sanctum/token', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
        'device_name' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['提供的凭据不正确。'],
        ]);
    }

    return $user->createToken($request->device_name)->plainTextToken;
});

当移动应用程序使用令牌向应用程序发出 API 请求时,应将令牌作为 Bearer 令牌传递在 Authorization 头中。

lightbulb

为移动应用程序颁发令牌时,您也可以自由指定 令牌能力

保护路由

如前所述,您可以通过将 sanctum 认证守卫附加到路由来保护路由,以便所有传入请求都必须经过认证:

php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

撤销令牌

为了允许用户撤销颁发给移动设备的 API 令牌,您可以在 Web 应用程序 UI 的“账户设置”部分中按名称列出它们,并附带一个“撤销”按钮。当用户单击“撤销”按钮时,您可以从数据库中删除令牌。请记住,您可以通过 Laravel\Sanctum\HasApiTokens trait 提供的 tokens 关系访问用户的 API 令牌:

php
// 撤销所有令牌...
$user->tokens()->delete();

// 撤销特定令牌...
$user->tokens()->where('id', $tokenId)->delete();

测试

在测试时,可以使用 Sanctum::actingAs 方法来认证用户并指定应授予其令牌的能力:

php
use App\Models\User;
use Laravel\Sanctum\Sanctum;

public function test_task_list_can_be_retrieved()
{
    Sanctum::actingAs(
        User::factory()->create(),
        ['view-tasks']
    );

    $response = $this->get('/api/task');

    $response->assertOk();
}

如果您希望将所有能力授予令牌,您应该在提供给 actingAs 方法的能力列表中包含 *

php
Sanctum::actingAs(
    User::factory()->create(),
    ['*']
);