Laravel Sanctum
介绍
Laravel Sanctum为 SPA(单页应用)、移动应用以及基于 token 的简单 API 提供了轻量级的身份验证系统。Sanctum 允许应用程序的每个用户为其帐户生成多个 API 令牌。这些令牌可以被授予权限/作用域,以指定允许令牌执行的操作。
工作原理
Laravel Sanctum 旨在解决两个不同的问题。在深入研究这个库之前,我们先来分别讨论一下这两个问题。
API 令牌
首先,Sanctum 是一个简单的软件包,您可以用它来向用户发放 API 令牌,而无需 OAuth 的复杂操作。此功能的灵感来自 GitHub 和其他发放“个人访问令牌”的应用程序。例如,假设您的应用程序的“帐户设置”中有一个界面,用户可以在其中为其帐户生成 API 令牌。您可以使用 Sanctum 来生成和管理这些令牌。这些令牌通常具有很长的有效期(数年),但用户可以随时手动撤销。
Laravel Sanctum 通过将用户 API 令牌存储在单个数据库表中并通过Authorization
应包含有效 API 令牌的标头对传入的 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 令牌。
Sanctum 仅用于 API 令牌认证或仅用于 SPA 认证完全没问题。使用 Sanctum 并不意味着您必须同时使用它提供的两个功能。
安装
您可以通过install:api
Artisan 命令安装 Laravel Sanctum:
1php artisan install:api
接下来,如果您计划利用 Sanctum 来验证 SPA,请参阅本文档的SPA 身份验证部分。
配置
覆盖默认模型
虽然通常不需要,但您可以自由扩展PersonalAccessToken
Sanctum 内部使用的模型:
1use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;2 3class PersonalAccessToken extends SanctumPersonalAccessToken4{5 // ...6}
usePersonalAccessTokenModel
然后,您可以通过Sanctum 提供的方法指示 Sanctum 使用您的自定义模型。通常,您应该在boot
应用程序AppServiceProvider
文件的方法中调用此方法:
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
令牌包含在标头中。
要开始为用户发放令牌,您的用户模型应该使用以下Laravel\Sanctum\HasApiTokens
特征:
1use Laravel\Sanctum\HasApiTokens;2 3class User extends Authenticatable4{5 use HasApiTokens, HasFactory, Notifiable;6}
要颁发令牌,您可以使用该createToken
方法。该createToken
方法返回一个Laravel\Sanctum\NewAccessToken
实例。API 令牌在存储到数据库之前会使用 SHA-256 哈希算法进行哈希处理,但您可以使用实例plainTextToken
的属性访问令牌的纯文本值NewAccessToken
。令牌创建后,您应该立即将此值显示给用户:
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});
tokens
您可以使用特征提供的 Eloquent 关系访问所有用户的令牌HasApiTokens
:
1foreach ($user->tokens as $token) {2 // ...3}
代币能力
Sanctum 允许你为令牌分配“权限”。权限的作用类似于 OAuth 的“作用域”。你可以将一个包含权限的字符串数组作为该createToken
方法的第二个参数传递:
1return $user->createToken('token-name', ['server:update'])->plainTextToken;
tokenCan
当处理由 Sanctum 验证的传入请求时,您可以使用或方法确定令牌是否具有给定的能力tokenCant
:
1if ($user->tokenCan('server:update')) {2 // ...3}4 5if ($user->tokenCant('server:update')) {6 // ...7}
Token 能力中间件
Sanctum 还包含两个中间件,可用于验证传入请求是否已使用已授予指定功能的令牌进行身份验证。首先,在应用程序bootstrap/app.php
文件中定义以下中间件别名:
1use Laravel\Sanctum\Http\Middleware\CheckAbilities;2use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;3 4->withMiddleware(function (Middleware $middleware) {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
然而,这并不一定意味着你的应用程序必须允许用户执行该操作。通常,你的应用程序的授权策略会判断令牌是否被授予了执行该功能的权限,并检查用户实例本身是否应该被允许执行该操作。
例如,如果我们想象一个管理服务器的应用程序,这可能意味着检查令牌是否被授权更新服务器以及服务器是否属于用户:
1return $request->user()->id === $server->user_id &&2 $request->user()->tokenCan('server:update')
乍一看,允许tokenCan
调用该方法并始终返回true
由第一方 UI 发起的请求可能看起来有些奇怪;但是,能够始终假设 API 令牌可用并且可以通过该tokenCan
方法进行检查会很方便。通过这种方式,您可以始终在应用的授权策略中调用该tokenCan
方法,而不必担心请求是由应用的 UI 触发的,还是由 API 的第三方使用者发起的。
保护路线
为了保护路由,确保所有传入的请求都必须经过身份验证,你应该在路由文件中的受保护路由上附加身份sanctum
验证防护。此防护将确保传入的请求被认证为有状态的请求、基于 Cookie 的请求,或者如果请求来自第三方,则包含有效的 API 令牌标头。routes/web.php
routes/api.php
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');
撤销令牌
tokens
您可以使用特征提供的关系从数据库中删除令牌来“撤销”令牌Laravel\Sanctum\HasApiTokens
:
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 令牌配置过期时间,则可以通过expiration
应用程序sanctum
配置文件中定义的配置选项进行配置。此配置选项定义了已颁发令牌被视为过期的分钟数:
1'expiration' => 525600,
如果您想要独立指定每个令牌的到期时间,您可以通过将到期时间作为createToken
方法的第三个参数来实现:
1return $user->createToken(2 'token-name', ['*'], now()->addWeek()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
标头以及Referer
或标头。Origin
配置
配置您的第一方域
首先,您应该配置您的 SPA 将从哪些域发出请求。您可以使用配置文件stateful
中的配置选项来配置这些域sanctum
。此配置设置决定了哪些域在向您的 API 发出请求时将使用 Laravel 会话 Cookie 来维持“有状态”身份验证。
为了帮助您设置第一方有状态域,Sanctum 提供了两个辅助函数,您可以将其包含在配置中。首先,Sanctum::currentApplicationUrlWithPort()
它将从环境变量返回当前应用程序 URL APP_URL
,并将Sanctum::currentRequestHost()
一个占位符注入有状态域列表,该占位符在运行时将被当前请求中的主机替换,以便所有具有相同域的请求都被视为有状态的。
如果您通过包含端口 () 的 URL 访问您的应用程序127.0.0.1:8000
,则应确保将端口号包含在域中。
Sanctum 中间件
接下来,你应该指示 Laravel,来自 SPA 的传入请求可以使用 Laravel 的会话 Cookie 进行身份验证,同时仍然允许来自第三方或移动应用程序的请求使用 API 令牌进行身份验证。这可以通过statefulApi
在应用程序bootstrap/app.php
文件中调用中间件方法轻松实现:
1->withMiddleware(function (Middleware $middleware) {2 $middleware->statefulApi();3})
CORS 和 Cookies
如果您在从在单独子域上执行的 SPA 对应用程序进行身份验证时遇到问题,则可能是您错误配置了 CORS(跨域资源共享)或会话 cookie 设置。
默认情况下,配置文件config/cors.php
不会发布。如果你需要自定义 Laravel 的 CORS 选项,则应cors
使用config:publish
Artisan 命令发布完整的配置文件:
1php artisan config:publish cors
接下来,你应该确保应用程序的 CORS 配置返回Access-Control-Allow-Credentials
值为 的标头True
。这可以通过将supports_credentials
应用程序config/cors.php
配置文件中的选项设置为 来实现true
。
此外,你应该在应用程序的全局实例上启用withCredentials
和选项。通常,这应该在你的文件中执行。如果你没有使用 Axios 从前端发出 HTTP 请求,则应该在你自己 HTTP 客户端上执行等效的配置:withXSRFToken
axios
resources/js/bootstrap.js
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 会设置一个XSRF-TOKEN
包含当前 CSRF 令牌的 Cookie。之后,此令牌需要进行 URL 解码,并在X-XSRF-TOKEN
后续请求的标头中传递。某些 HTTP 客户端库(例如 Axios 和 Angular HttpClient)会自动执行此操作。如果您的 JavaScript HTTP 库未为您设置此值,则需要手动设置标头,使其与此路由设置的 CookieX-XSRF-TOKEN
的 URL 解码值匹配。XSRF-TOKEN
登录
初始化 CSRF 保护后,你应该POST
向 Laravel 应用程序的/login
路由发出请求。此/login
路由可以手动实现,也可以使用像Laravel Fortify这样的无头身份验证包。
如果登录请求成功,您将通过身份验证,并且后续对应用程序路由的请求将自动通过 Laravel 应用程序发给客户端的会话 cookie 进行身份验证。此外,由于您的应用程序已经向该/sanctum/csrf-cookie
路由发出了请求,因此只要您的 JavaScript HTTP 客户端XSRF-TOKEN
在X-XSRF-TOKEN
标头中发送 cookie 的值,后续请求就会自动受到 CSRF 保护。
当然,如果用户的会话由于缺乏Events而过期,则对 Laravel 应用程序的后续请求可能会收到 401 或 419 HTTP 错误响应。在这种情况下,您应该将用户重定向到 SPA 的登录页面。
您可以自由编写自己的端点;但是,您应该确保它使用Laravel 提供的/login
标准、基于会话的身份验证服务对用户进行身份验证。通常,这意味着使用身份验证保护。web
保护路线
为了保护路由,确保所有传入的请求都必须经过身份验证,您应该在文件sanctum
中将身份验证保护附加到 API 路由routes/api.php
。此保护将确保传入的请求要么被认证为来自 SPA 的有状态身份验证请求,要么在请求来自第三方时包含有效的 API 令牌标头:
1use Illuminate\Http\Request;2 3Route::get('/user', function (Request $request) {4 return $request->user();5})->middleware('auth:sanctum');
授权私人广播频道
如果您的 SPA 需要使用private / presence 广播频道进行身份验证,则应channels
从withRouting
应用程序bootstrap/app.php
文件中包含的方法中删除该条目。相反,您应该调用该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 的授权请求成功,你需要authorizer
在初始化Laravel Echo时提供一个自定义 Pusher 。这允许你的应用程序将 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.name12 })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 12”。
通常,您会从移动应用程序的“登录”屏幕向令牌端点发出请求。该端点将返回纯文本 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 的“帐户设置”部分按名称列出这些令牌,并添加“撤销”按钮。当用户点击“撤销”按钮时,您可以从数据库中删除该令牌。请记住,您可以通过traittokens
提供的关系访问用户的 API 令牌Laravel\Sanctum\HasApiTokens
:
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);