跳至内容

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 令牌。

lightbulb

仅将 Sanctum 用于 API 令牌身份验证或仅用于 SPA 身份验证都完全没问题。仅仅因为您使用了 Sanctum,并不意味着您必须使用它提供的两种功能。

安装

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

php artisan install:api

接下来,如果您计划利用 Sanctum 对 SPA 进行身份验证,请参阅本文档的SPA 身份验证部分。

配置

覆盖默认模型

尽管通常不需要,但您可以扩展 Sanctum 内部使用的PersonalAccessToken 模型。

use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
 
class PersonalAccessToken extends SanctumPersonalAccessToken
{
// ...
}

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

use App\Models\Sanctum\PersonalAccessToken;
use Laravel\Sanctum\Sanctum;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}

API 令牌身份验证

lightbulb

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

颁发 API 令牌

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

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

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

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

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 关系访问用户的所有令牌。

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

令牌权限

Sanctum 允许您为令牌分配“权限”。权限与 OAuth 的“范围”具有相似的用途。您可以将字符串权限数组作为第二个参数传递给createToken 方法。

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

在处理由 Sanctum 身份验证的传入请求时,您可以使用tokenCan 方法确定令牌是否具有给定的权限。

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

令牌权限中间件

Sanctum 还包含两个中间件,可用于验证传入请求是否已使用已授予给定权限的令牌进行身份验证。要开始使用,请在应用程序的bootstrap/app.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 中间件分配给路由,以验证传入请求的令牌是否具有所有列出的权限。

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

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

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

第一方 UI 发起的请求

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

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

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

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方法。

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

吊销令牌

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

// Revoke all tokens...
$user->tokens()->delete();
 
// Revoke the token that was used to authenticate the current request...
$request->user()->currentAccessToken()->delete();
 
// Revoke a specific token...
$user->tokens()->where('id', $tokenId)->delete();

令牌过期

默认情况下,Sanctum 令牌永不过期,并且只能通过撤销令牌来使令牌失效。但是,如果您希望为应用程序的 API 令牌配置过期时间,则可以通过应用程序的sanctum配置文件中定义的expiration配置选项来实现。此配置选项定义了颁发令牌后多长时间将被视为过期。

'expiration' => 525600,

如果您希望独立指定每个令牌的过期时间,则可以通过将过期时间作为第三个参数提供给createToken方法来实现。

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

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

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中间件方法轻松实现。

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

CORS 和 Cookie

如果您在从在单独子域上执行的 SPA 对应用程序进行身份验证时遇到问题,则可能是您错误地配置了 CORS(跨源资源共享)或会话 Cookie 设置。

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

php artisan config:publish cors

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

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

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

最后,您应该确保应用程序的会话 Cookie 域名配置支持根域名的任何子域名。您可以在应用程序的config/session.php配置文件中在域名前加上一个前导.来实现此目的。

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

身份验证

CSRF 保护

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

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

在此请求期间,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 令牌标头。

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

授权私有广播频道

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

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时提供自定义的 Pusherauthorizer。这允许您的应用程序将 Pusher 配置为使用已正确配置用于跨域请求的axios实例

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 请求。

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标头中。

lightbulb

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

保护路由

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

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

吊销令牌

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

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

测试

在测试期间,可以使用Sanctum::actingAs方法对用户进行身份验证并指定应授予其令牌哪些功能。

use App\Models\User;
use Laravel\Sanctum\Sanctum;
 
test('task list can be retrieved', function () {
Sanctum::actingAs(
User::factory()->create(),
['view-tasks']
);
 
$response = $this->get('/api/task');
 
$response->assertOk();
});
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方法的功能列表中包含*

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