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 应用程序相同的代码库中,或者可能是一个完全独立的代码库,例如使用 Next.js 或 Nuxt 创建的 SPA。

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

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

lightbulb

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

安装

您可以通过 install:api Artisan 命令安装 Laravel Sanctum:

shell
php artisan install:api

接下来,如果您计划利用 Sanctum 来认证 SPA,请参考本文档的 SPA 认证 部分。

配置

覆盖默认模型

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

php
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
{
    // ...
}

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

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

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

API 令牌认证

lightbulb

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

发放 API 令牌

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

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

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 特性提供的 tokens Eloquent 关系访问用户的所有令牌:

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

令牌能力

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

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

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

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

if ($user->tokenCant('server:update')) {
    // ...
}

令牌能力中间件

Sanctum 还包括两个中间件,可用于验证传入请求是否经过认证,并且令牌已被授予给定能力。要开始,请在应用程序的 bootstrap/app.php 文件中定义以下中间件别名:

php
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'abilities' => CheckAbilities::class,
        'ability' => 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 方法被调用并始终返回 true 对于第一方 UI 发起的请求可能看起来很奇怪;然而,能够始终假设 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::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

撤销令牌

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

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

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

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

令牌过期

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

php
'expiration' => 525600,

如果您希望为每个令牌单独指定过期时间,可以通过将过期时间作为第三个参数传递给 createToken 方法来实现:

php
return $user->createToken(
    'token-name', ['*'], now()->addWeek()
)->plainTextToken;

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

php
use Illuminate\Support\Facades\Schedule;

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 头部和 RefererOrigin 头部。

配置

配置您的第一方域名

首先,您应该配置您的 SPA 将从哪些域名发起请求。您可以使用应用程序的 sanctum 配置文件中的 stateful 配置选项来配置这些域名。此配置设置决定了哪些域名在向您的 API 发起请求时将保持“状态”的认证,使用 Laravel 会话 cookie。

exclamation

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

Sanctum 中间件

接下来,您应该指示 Laravel,来自您的 SPA 的传入请求可以使用 Laravel 的会话 cookie 进行认证,同时仍允许来自第三方或移动应用的请求使用 API 令牌进行认证。这可以通过在应用程序的 bootstrap/app.php 文件中调用 statefulApi 中间件方法轻松实现:

php
->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

CORS 和 Cookies

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

config/cors.php 配置文件默认未发布。如果您需要自定义 Laravel 的 CORS 选项,您应使用 config:publish Artisan 命令发布完整的 cors 配置文件:

bash
php artisan config:publish cors

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

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

js
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = 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。随后,该令牌应被 URL 解码并在后续请求中通过 X-XSRF-TOKEN 请求头传递。一些 HTTP 客户端库(如 Axios 和 Angular 的 HttpClient)会自动为您完成此操作。如果您的 JavaScript HTTP 库未自动设置该值,则需要手动将 X-XSRF-TOKEN 请求头设置为与该路由设置的 XSRF-TOKEN Cookie 的 URL 解码值相匹配。

登录

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

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

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

exclamation

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

保护路由

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

php
use Illuminate\Http\Request;

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

授权私有广播频道

如果您的 SPA 需要与 私有/存在广播频道 进行认证,您应从应用程序的 bootstrap/app.php 文件中的 withRouting 方法中删除 channels 条目。相反,您应调用 withBroadcasting 方法,以便您可以为应用程序的广播路由指定正确的中间件:

php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        // ...
    )
    ->withBroadcasting(
        __DIR__.'/../routes/channels.php',
        ['prefix' => 'api', 'middleware' => ['api', '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 令牌。此端点给出的“设备名称”仅供参考,您可以使用任何您希望的值。通常,设备名称值应该是用户可以识别的名称,例如“Nuno 的 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::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

撤销令牌

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

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

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

测试

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

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

test('任务列表可以被检索', function () {
    Sanctum::actingAs(
        User::factory()->create(),
        ['view-tasks']
    );

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

    $response->assertOk();
});
php
use App\Models\User;
use Laravel\Sanctum\Sanctum;

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

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

    $response->assertOk();
}

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

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