跳转至内容

Laravel Sanctum

简介

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

工作原理

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

API 令牌

首先,Sanctum 是一个简单的包,你可以用它来为用户颁发 API 令牌,而无需复杂的 OAuth。这一功能受到 GitHub 和其他颁发“个人访问令牌”(personal access tokens)应用的启发。例如,想象你的应用的“账户设置”页面中有一个界面,用户可以在其中为自己的账户生成 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 认证守卫(guard)来实现这一点。这提供了 CSRF 保护、会话认证的好处,并能防止通过 XSS 泄露认证凭据。

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

仅将 Sanctum 用于 API 令牌认证或仅用于 SPA 认证是完全没问题的。使用 Sanctum 并不意味着你必须同时使用它提供的这两个功能。

安装

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

1php artisan install:api

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

配置

覆盖默认模型

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

1use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
2 
3class PersonalAccessToken extends SanctumPersonalAccessToken
4{
5 // ...
6}

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

1use App\Models\Sanctum\PersonalAccessToken;
2use Laravel\Sanctum\Sanctum;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
10}

API 令牌认证

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

颁发 API 令牌

Sanctum 允许你颁发可用于认证 API 请求的 API 令牌(个人访问令牌)。当使用 API 令牌发起请求时,令牌应包含在 Authorization 请求头中,作为 Bearer 令牌。

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

1use Laravel\Sanctum\HasApiTokens;
2 
3class User extends Authenticatable
4{
5 use HasApiTokens, HasFactory, Notifiable;
6}

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

1use Illuminate\Http\Request;
2 
3Route::post('/tokens/create', function (Request $request) {
4 $token = $request->user()->createToken($request->token_name);
5 
6 return ['token' => $token->plainTextToken];
7});

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

1foreach ($user->tokens as $token) {
2 // ...
3}

令牌能力 (Abilities)

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

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

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

1if ($user->tokenCan('server:update')) {
2 // ...
3}
4 
5if ($user->tokenCant('server:update')) {
6 // ...
7}

令牌能力中间件

Sanctum 还包含两个中间件,可用于验证传入请求是否由被授予特定能力的令牌所认证。首先,在你的应用程序 bootstrap/app.php 文件中定义以下中间件别名:

1use Laravel\Sanctum\Http\Middleware\CheckAbilities;
2use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
3 
4->withMiddleware(function (Middleware $middleware): void {
5 $middleware->alias([
6 'abilities' => CheckAbilities::class,
7 'ability' => CheckForAnyAbility::class,
8 ]);
9})

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

1Route::get('/orders', function () {
2 // Token has both "check-status" and "place-orders" abilities...
3})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);

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

1Route::get('/orders', function () {
2 // Token has the "check-status" or "place-orders" ability...
3})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);

第一方 UI 发起的请求

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

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

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

1return $request->user()->id === $server->user_id &&
2 $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 方法。

1use Illuminate\Http\Request;
2 
3Route::get('/user', function (Request $request) {
4 return $request->user();
5})->middleware('auth:sanctum');

撤销令牌

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

1// Revoke all tokens...
2$user->tokens()->delete();
3 
4// Revoke the token that was used to authenticate the current request...
5$request->user()->currentAccessToken()->delete();
6 
7// Revoke a specific token...
8$user->tokens()->where('id', $tokenId)->delete();

令牌过期

默认情况下,Sanctum 令牌永不过期,只能通过撤销令牌使其失效。但是,如果你想为应用程序的 API 令牌配置过期时间,可以通过应用程序 sanctum 配置文件中定义的 expiration 配置选项进行设置。此配置选项定义了已颁发的令牌在过期前的分钟数。

1'expiration' => 525600,

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

1return $user->createToken(
2 'token-name', ['*'], now()->plus(weeks: 1)
3)->plainTextToken;

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

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('sanctum:prune-expired --hours=24')->daily();

SPA 认证

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

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

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

配置

配置第一方域名

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

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

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

Sanctum 中间件

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

1->withMiddleware(function (Middleware $middleware): void {
2 $middleware->statefulApi();
3})

CORS 和 Cookie

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

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

1php artisan config:publish cors

接下来,你应该确保应用程序的 CORS 配置返回值为 TrueAccess-Control-Allow-Credentials 头。这可以通过将应用程序 config/cors.php 配置文件中的 supports_credentials 选项设置为 true 来完成。

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

1axios.defaults.withCredentials = true;
2axios.defaults.withXSRFToken = true;

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

1'domain' => '.domain.com',

进行认证

CSRF 保护

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

1axios.get('/sanctum/csrf-cookie').then(response => {
2 // Login...
3});

在此请求期间,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 路由发起过请求,只要你的 JavaScript HTTP 客户端在 X-XSRF-TOKEN 头中发送 XSRF-TOKEN Cookie 的值,随后的请求就应该自动获得 CSRF 保护。

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

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

保护路由

为了保护路由以确保所有传入请求都必须经过认证,你应该在 routes/api.php 文件中将 sanctum 认证守卫附加到你的 API 路由上。此守卫将确保传入请求要么作为来自 SPA 的有状态认证请求进行认证,要么在请求来自第三方时包含有效的 API 令牌头。

1use Illuminate\Http\Request;
2 
3Route::get('/user', function (Request $request) {
4 return $request->user();
5})->middleware('auth:sanctum');

授权私有广播频道

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

1return Application::configure(basePath: dirname(__DIR__))
2 ->withRouting(
3 web: __DIR__.'/../routes/web.php',
4 // ...
5 )
6 ->withBroadcasting(
7 __DIR__.'/../routes/channels.php',
8 ['prefix' => 'api', 'middleware' => ['api', 'auth:sanctum']],
9 )

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

1window.Echo = new Echo({
2 broadcaster: "pusher",
3 cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
4 encrypted: true,
5 key: import.meta.env.VITE_PUSHER_APP_KEY,
6 authorizer: (channel, options) => {
7 return {
8 authorize: (socketId, callback) => {
9 axios.post('/api/broadcasting/auth', {
10 socket_id: socketId,
11 channel_name: channel.name
12 })
13 .then(response => {
14 callback(false, response.data);
15 })
16 .catch(error => {
17 callback(true, error);
18 });
19 }
20 };
21 },
22})

移动应用认证

你还可以使用 Sanctum 令牌来认证移动应用程序向 API 发起的请求。认证移动应用请求的过程与认证第三方 API 请求的过程类似;但在颁发 API 令牌的方式上存在微小差异。

颁发 API 令牌

首先,创建一个接受用户电子邮件/用户名、密码和设备名称的路由,然后将这些凭据交换为新的 Sanctum 令牌。提供给此端点的“设备名称”仅供参考,可以是任何你想要的值。通常,设备名称应该是用户能够识别的名称,例如“Nuno 的 iPhone 17”。

通常,你会从移动应用程序的“登录”屏幕向令牌端点发起请求。该端点将返回明文 API 令牌,然后可以将其存储在移动设备上,并用于发起后续的 API 请求。

1use App\Models\User;
2use Illuminate\Http\Request;
3use Illuminate\Support\Facades\Hash;
4use Illuminate\Validation\ValidationException;
5 
6Route::post('/sanctum/token', function (Request $request) {
7 $request->validate([
8 'email' => 'required|email',
9 'password' => 'required',
10 'device_name' => 'required',
11 ]);
12 
13 $user = User::where('email', $request->email)->first();
14 
15 if (! $user || ! Hash::check($request->password, $user->password)) {
16 throw ValidationException::withMessages([
17 'email' => ['The provided credentials are incorrect.'],
18 ]);
19 }
20 
21 return $user->createToken($request->device_name)->plainTextToken;
22});

当移动应用程序使用令牌向你的应用程序发起 API 请求时,它应该在 Authorization 头中以 Bearer 令牌的形式传递令牌。

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

保护路由

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

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

撤销令牌

为了允许用户撤销颁发给移动设备的 API 令牌,你可以在 Web 应用 UI 的“账户设置”部分列出它们,并提供一个“撤销”按钮。当用户点击“撤销”按钮时,你可以从数据库中删除该令牌。请记住,你可以通过 HasApiTokens trait 提供的 tokens 关联访问用户的 API 令牌。

1// Revoke all tokens...
2$user->tokens()->delete();
3 
4// Revoke a specific token...
5$user->tokens()->where('id', $tokenId)->delete();

测试

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

1use App\Models\User;
2use Laravel\Sanctum\Sanctum;
3 
4test('task list can be retrieved', function () {
5 Sanctum::actingAs(
6 User::factory()->create(),
7 ['view-tasks']
8 );
9 
10 $response = $this->get('/api/task');
11 
12 $response->assertOk();
13});
1use App\Models\User;
2use Laravel\Sanctum\Sanctum;
3 
4public function test_task_list_can_be_retrieved(): void
5{
6 Sanctum::actingAs(
7 User::factory()->create(),
8 ['view-tasks']
9 );
10 
11 $response = $this->get('/api/task');
12 
13 $response->assertOk();
14}

如果你想向令牌授予所有能力,你应该在传递给 actingAs 方法的能力列表中包含 *

1Sanctum::actingAs(
2 User::factory()->create(),
3 ['*']
4);