Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

Laravel Sanctum

简介

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

工作原理

Laravel Sanctum 的存在是为了解决两个独立的问题。在深入研究该库之前,我们先分别讨论一下。

API 令牌

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

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

SPA 认证

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

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

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

NOTE

完全可以只将 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 令牌认证

NOTE

你不应使用 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 认证的传入请求时,你可以使用 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): void {
    $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 方法被调用,并且对于第一方 UI 发起的请求始终返回 true 可能看起来很奇怪;但是,能够始终假设 API 令牌可用并可以通过 tokenCan 方法进行检查是很方便的。通过采用这种方法,你可以在应用的授权策略中始终调用 tokenCan 方法,而无需担心该请求是从应用的 UI 触发的,还是由你的 API 的第三方消费者发起的。

保护路由

为了保护路由,使得所有传入请求都必须经过认证,你应该在你的 routes/web.phproutes/api.php 路由文件中,将 sanctum 认证守卫附加到你的受保护路由上。此守卫将确保传入请求要么通过有状态的、Cookie 认证的请求进行认证,要么(如果请求来自第三方)包含一个有效的 API 令牌头。

你可能想知道为什么我们建议使用 sanctum 守卫来认证你应用的 routes/web.php 文件中的路由。记住,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 trait 提供的 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()->plus(weeks: 1)
)->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 泄露。

WARNING

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

配置

配置第一方域

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

为了帮助你设置第一方有状态域,Sanctum 提供了两个辅助函数,你可以将其包含在配置中。首先,Sanctum::currentApplicationUrlWithPort() 将返回来自 APP_URL 环境变量的当前应用 URL,而 Sanctum::currentRequestHost() 将向有状态域列表注入一个占位符,该占位符在运行时将被当前请求的主机替换,以便所有具有相同域的请求都被视为有状态。

WARNING

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

Sanctum 中间件

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

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

如果你在从运行在单独子域上的 SPA 认证你的应用时遇到问题,很可能是因为你的 CORS(跨源资源共享)或会话 Cookie 设置配置错误。

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

shell
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 头中,像 Axios 和 Angular HttpClient 等一些 HTTP 客户端库会自动为你执行此操作。如果你的 JavaScript HTTP 库没有为你设置该值,你需要手动将 X-XSRF-TOKEN 头设置为与该路由设置的 XSRF-TOKEN Cookie 的 URL 解码值匹配。

登录

一旦 CSRF 保护初始化完成,你应该向你的 Laravel 应用的 /login 路由发起 POST 请求。这个 /login 路由可以 手动实现,也可以使用像 Laravel Fortify 这样的无头认证包。

如果登录请求成功,你将通过认证,并且对应用路由的后续请求将自动通过 Laravel 应用发给你的客户端所设置的会话 Cookie 进行认证。此外,由于你的应用已经向 /sanctum/csrf-cookie 路由发起过请求,只要你的 JavaScript HTTP 客户端将 XSRF-TOKEN Cookie 的值在 X-XSRF-TOKEN 头中发送,后续请求将自动受到 CSRF 保护。

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

WARNING

你可以自由地编写自己的 /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 17”。

通常,你将通过移动应用的“登录”屏幕向令牌端点发起请求。该端点将返回明文 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' => ['The provided credentials are incorrect.'],
        ]);
    }

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

当移动应用使用该令牌向你的应用发起 API 请求时,它应将令牌作为 Bearer 令牌放在 Authorization 头中传递。

NOTE

在为移动应用发放令牌时,你也可以自由地指定 令牌能力

保护路由

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

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

撤销令牌

为了允许用户撤销发放给移动设备的 API 令牌,你可以在你的 Web 应用界面的“账户设置”部分,按名称列出它们,并附带一个“撤销”按钮。当用户点击“撤销”按钮时,你可以从数据库中删除该令牌。请记住,你可以通过 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;

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(),
    ['*']
);