跳轉至內容

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);