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 SanctumPersonalAccessToken4{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 Authenticatable4{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 认证的传入请求时,你可以使用 tokenCan 或 tokenCant 方法确定令牌是否具备给定的能力:
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.php 和 routes/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 头以及 Referer 或 Origin 头。
配置
配置第一方域名
首先,你应该配置 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 配置返回值为 True 的 Access-Control-Allow-Credentials 头。这可以通过将应用程序 config/cors.php 配置文件中的 supports_credentials 选项设置为 true 来完成。
此外,你应该在应用程序的全局 axios 实例上启用 withCredentials 和 withXSRFToken 选项。通常,这应该在 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.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 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);