簡介 (Introduction)
Laravel Sanctum 為 SPA (單頁應用程式)、行動應用程式和簡單的 Token 基礎 API 提供了一個輕量級的驗證系統。Sanctum 允許應用程式的每個使用者為其帳戶產生多個 API Token。這些 Token 可以被授予能力 / 範圍 (abilities / scopes),以指定 Token 被允許執行的動作。
運作原理 (How it Works)
Laravel Sanctum 的存在是為了解決兩個分開的問題。在深入探討這個函式庫之前,讓我們先討論每一個問題。
API Tokens
首先,Sanctum 是一個簡單的套件,你可以用它來發行 API Token 給你的使用者,而不需要 OAuth 的複雜性。這個功能的靈感來自 GitHub 和其他發行「個人存取 Token (Personal Access Tokens)」的應用程式。例如,想像你的應用程式的「帳戶設定」有一個畫面,使用者可以在那裡為他們的帳戶產生一個 API Token。你可以使用 Sanctum 來產生和管理這些 Token。這些 Token 通常有很長的過期時間 (數年),但使用者可以隨時手動撤銷。
Laravel Sanctum 透過將使用者 API Token 儲存在單一資料庫資料表中,並透過應包含有效 API Token 的 Authorization 標頭來驗證傳入的 HTTP 請求,從而提供此功能。
SPA 驗證 (SPA Authentication)
其次,Sanctum 的存在是為了提供一種簡單的方法來驗證需要與 Laravel 驅動的 API 進行通訊的單頁應用程式 (SPA)。這些 SPA 可能存在於與你的 Laravel 應用程式相同的儲存庫中,或者可能是一個完全獨立的儲存庫,例如使用 Next.js 或 Nuxt 建立的 SPA。
對於此功能,Sanctum 不使用任何種類的 Token。相反地,Sanctum 使用 Laravel 內建的 Cookie 基礎 Session 驗證服務。通常,Sanctum 利用 Laravel 的 web 驗證 Guard 來完成此任務。這提供了 CSRF 保護、Session 驗證的好處,並防止驗證憑證透過 XSS 洩漏。
當傳入的請求源自你自己的 SPA 前端時,Sanctum 只會嘗試使用 Cookie 進行驗證。當 Sanctum 檢查傳入的 HTTP 請求時,它會先檢查是否有驗證 Cookie,如果沒有,Sanctum 接著會檢查 Authorization 標頭是否有有效的 API Token。
[!NOTE] 僅將 Sanctum 用於 API Token 驗證或僅用於 SPA 驗證是完全沒問題的。僅僅因為你使用了 Sanctum,並不意味著你必須使用它提供的這兩個功能。
安裝 (Installation)
你可以透過 install:api Artisan 指令安裝 Laravel Sanctum:
php artisan install:api
接下來,如果你打算利用 Sanctum 來驗證 SPA,請參考本文件的 SPA 驗證 章節。
設定 (Configuration)
覆寫預設 Model (Overriding Default Models)
雖然通常不需要,但你可以自由擴充 Sanctum 內部使用的 PersonalAccessToken Model:
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
// ...
}
然後,你可以透過 Sanctum 提供的 usePersonalAccessTokenModel 方法指示 Sanctum 使用你的自訂 Model。通常,你應該在應用程式的 AppServiceProvider 檔案的 boot 方法中呼叫此方法:
use App\Models\Sanctum\PersonalAccessToken;
use Laravel\Sanctum\Sanctum;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}
API Token 驗證 (API Token Authentication)
[!NOTE] 你不應該使用 API Token 來驗證你自己的第一方 SPA。相反地,請使用 Sanctum 內建的 SPA 驗證功能。
發行 API Token (Issuing API Tokens)
Sanctum 允許你發行 API Token / 個人存取 Token,這些 Token 可用於驗證對你應用程式的 API 請求。使用 API Token 發出請求時,應將 Token 包含在 Authorization 標頭中作為 Bearer Token。
要開始為使用者發行 Token,你的 User Model 應該使用 Laravel\Sanctum\HasApiTokens Trait:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
要發行 Token,你可以使用 createToken 方法。createToken 方法回傳一個 Laravel\Sanctum\NewAccessToken 實例。API Token 在儲存到資料庫之前會使用 SHA-256 雜湊進行雜湊處理,但你可以使用 NewAccessToken 實例的 plainTextToken 屬性存取 Token 的純文字值。你應該在 Token 建立後立即向使用者顯示此值:
use Illuminate\Http\Request;
Route::post('/tokens/create', function (Request $request) {
$token = $request->user()->createToken($request->token_name);
return ['token' => $token->plainTextToken];
});
你可以使用 HasApiTokens Trait 提供的 tokens Eloquent 關聯存取使用者的所有 Token:
foreach ($user->tokens as $token) {
// ...
}
Token 能力 (Token Abilities)
Sanctum 允許你指派「能力 (abilities)」給 Token。能力的作用類似於 OAuth 的「範圍 (scopes)」。你可以將字串能力陣列作為第二個參數傳遞給 createToken 方法:
return $user->createToken('token-name', ['server:update'])->plainTextToken;
當處理經由 Sanctum 驗證的傳入請求時,你可以使用 tokenCan 或 tokenCant 方法判斷 Token 是否具有給定的能力:
if ($user->tokenCan('server:update')) {
// ...
}
if ($user->tokenCant('server:update')) {
// ...
}
Token 能力 Middleware (Token Ability Middleware)
Sanctum 還包含兩個 Middleware,可用於驗證傳入的請求是否使用已授予給定能力的 Token 進行驗證。首先,在應用程式的 bootstrap/app.php 檔案中定義以下 Middleware 別名:
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
]);
})
abilities Middleware 可以指派給路由,以驗證傳入請求的 Token 是否具有所有列出的能力:
Route::get('/orders', function () {
// Token has both "check-status" and "place-orders" abilities...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);
ability Middleware 可以指派給路由,以驗證傳入請求的 Token 是否具有列出的能力中的 至少一個:
Route::get('/orders', function () {
// Token has the "check-status" or "place-orders" ability...
})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);
第一方 UI 發起的請求 (First-Party UI Initiated Requests)
為了方便起見,如果傳入的驗證請求來自你的第一方 SPA,並且你正在使用 Sanctum 內建的 SPA 驗證,tokenCan 方法將始終回傳 true。
然而,這並不一定意味著你的應用程式必須允許使用者執行該動作。通常,你的應用程式的 Authorization Policies 將決定 Token 是否已被授予執行能力的權限,並檢查使用者實例本身是否應被允許執行該動作。
例如,如果我們想像一個管理伺服器的應用程式,這可能意味著檢查 Token 是否被授權更新伺服器 並且 伺服器屬於該使用者:
return $request->user()->id === $server->user_id &&
$request->user()->tokenCan('server:update')
起初,允許呼叫 tokenCan 方法並對第一方 UI 發起的請求始終回傳 true 似乎很奇怪;然而,能夠始終假設 API Token 可用並可透過 tokenCan 方法進行檢查是很方便的。透過採取這種方法,你可以在應用程式的 Authorization Policies 中始終呼叫 tokenCan 方法,而不必擔心請求是從應用程式的 UI 觸發的,還是由你的 API 的第三方消費者發起的。
保護路由 (Protecting Routes)
為了保護路由,使所有傳入的請求都必須經過驗證,你應該將 sanctum 驗證 Guard 附加到 routes/web.php 和 routes/api.php 路由檔案中的受保護路由。此 Guard 將確保傳入的請求被驗證為有狀態的、Cookie 驗證的請求,或者如果請求來自第三方,則包含有效的 API Token 標頭。
你可能想知道為什麼我們建議你使用 sanctum Guard 來驗證應用程式 routes/web.php 檔案中的路由。請記住,Sanctum 會先嘗試使用 Laravel 典型的 Session 驗證 Cookie 來驗證傳入的請求。如果該 Cookie 不存在,Sanctum 將嘗試使用請求的 Authorization 標頭中的 Token 來驗證請求。此外,使用 Sanctum 驗證所有請求可確保我們始終可以在當前驗證的使用者實例上呼叫 tokenCan 方法:
use Illuminate\Http\Request;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
撤銷 Token (Revoking Tokens)
你可以透過使用 Laravel\Sanctum\HasApiTokens Trait 提供的 tokens 關聯從資料庫中刪除 Token 來「撤銷」它們:
// 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();
Token 過期 (Token Expiration)
預設情況下,Sanctum Token 永不過期,只能透過 撤銷 Token 來使其無效。然而,如果你想為應用程式的 API Token 設定過期時間,你可以透過應用程式 sanctum 設定檔中定義的 expiration 設定選項來實現。此設定選項定義了發行的 Token 被視為過期之前的分鐘數:
'expiration' => 525600,
如果你想獨立指定每個 Token 的過期時間,你可以透過將過期時間作為第三個參數提供給 createToken 方法來實現:
return $user->createToken(
'token-name', ['*'], now()->addWeek()
)->plainTextToken;
如果你為應用程式設定了 Token 過期時間,你可能還希望 排程任務 來修剪應用程式的過期 Token。幸運的是,Sanctum 包含一個 sanctum:prune-expired Artisan 指令,你可以用它來完成此任務。例如,你可以設定一個排程任務來刪除所有已過期至少 24 小時的過期 Token 資料庫記錄:
use Illuminate\Support\Facades\Schedule;
Schedule::command('sanctum:prune-expired --hours=24')->daily();
SPA 驗證 (SPA Authentication)
Sanctum 的存在也是為了提供一種簡單的方法來驗證需要與 Laravel 驅動的 API 進行通訊的單頁應用程式 (SPA)。這些 SPA 可能存在於與你的 Laravel 應用程式相同的儲存庫中,或者可能是一個完全獨立的儲存庫。
對於此功能,Sanctum 不使用任何種類的 Token。相反地,Sanctum 使用 Laravel 內建的 Cookie 基礎 Session 驗證服務。這種驗證方法提供了 CSRF 保護、Session 驗證的好處,並防止驗證憑證透過 XSS 洩漏。
[!WARNING] 為了進行驗證,你的 SPA 和 API 必須共享相同的頂級網域。但是,它們可以位於不同的子網域上。此外,你應該確保在請求中發送
Accept: application/json標頭以及Referer或Origin標頭。
設定 (Configuration)
設定你的第一方網域 (Configuring Your First-Party Domains)
首先,你應該設定你的 SPA 將從哪些網域發出請求。你可以在 sanctum 設定檔中使用 stateful 設定選項來設定這些網域。此設定決定了哪些網域在向你的 API 發出請求時,將使用 Laravel Session Cookie 維持「有狀態 (stateful)」驗證。
為了協助你設定第一方有狀態網域,Sanctum 提供了兩個輔助函式,你可以將其包含在設定中。首先,Sanctum::currentApplicationUrlWithPort() 將回傳 APP_URL 環境變數中的當前應用程式 URL,而 Sanctum::currentRequestHost() 將在有狀態網域列表中注入一個佔位符,該佔位符在執行時將被當前請求的主機替換,以便所有具有相同網域的請求都被視為有狀態。
[!WARNING] 如果你是透過包含連接埠的 URL (
127.0.0.1:8000) 存取你的應用程式,你應該確保將連接埠號包含在網域中。
Sanctum Middleware
接下來,你應該指示 Laravel,來自 SPA 的傳入請求可以使用 Laravel 的 Session Cookie 進行驗證,同時仍允許來自第三方或行動應用程式的請求使用 API Token 進行驗證。這可以透過在應用程式的 bootstrap/app.php 檔案中呼叫 statefulApi Middleware 方法輕鬆完成:
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi();
})
CORS 和 Cookies (CORS and Cookies)
如果你在從執行於單獨子網域的 SPA 驗證應用程式時遇到問題,你可能錯誤設定了 CORS (跨來源資源共用) 或 Session Cookie 設定。
config/cors.php 設定檔預設未發佈。如果你需要自訂 Laravel 的 CORS 選項,你應該使用 config:publish Artisan 指令發佈完整的 cors 設定檔:
php 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 用戶端上執行等效的設定:
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;
最後,你應該確保應用程式的 Session Cookie 網域設定支援你的根網域的任何子網域。你可以透過在應用程式的 config/session.php 設定檔中為網域加上前導 . 來完成此操作:
'domain' => '.domain.com',
驗證 (Authenticating)
CSRF 保護 (CSRF Protection)
要驗證你的 SPA,你的 SPA 的「登入」頁面應首先向 /sanctum/csrf-cookie 端點發出請求,以初始化應用程式的 CSRF 保護:
axios.get("/sanctum/csrf-cookie").then((response) => {
// Login...
});
在此請求期間,Laravel 將設定一個包含當前 CSRF Token 的 XSRF-TOKEN Cookie。然後,此 Token 應進行 URL 解碼並在後續請求的 X-XSRF-TOKEN 標頭中傳遞,某些 HTTP 用戶端函式庫 (如 Axios 和 Angular HttpClient) 會自動為你執行此操作。如果你的 JavaScript HTTP 函式庫沒有為你設定該值,你需要手動設定 X-XSRF-TOKEN 標頭以符合此路由設定的 XSRF-TOKEN Cookie 的 URL 解碼值。
登入 (Logging In)
一旦 CSRF 保護初始化完成,你應該向 Laravel 應用程式的 /login 路由發出 POST 請求。此 /login 路由可以 手動實作 或使用像 Laravel Fortify 這樣的無頭驗證套件。
如果登入請求成功,你將被驗證,並且對應用程式路由的後續請求將透過 Laravel 應用程式發給用戶端的 Session Cookie 自動進行驗證。此外,由於你的應用程式已經向 /sanctum/csrf-cookie 路由發出請求,只要你的 JavaScript HTTP 用戶端在 X-XSRF-TOKEN 標頭中發送 XSRF-TOKEN Cookie 的值,後續請求應自動獲得 CSRF 保護。
當然,如果使用者的 Session 由於缺乏活動而過期,對 Laravel 應用程式的後續請求可能會收到 401 或 419 HTTP 錯誤回應。在這種情況下,你應該將使用者重新導向到 SPA 的登入頁面。
[!WARNING] 你可以自由編寫自己的
/login端點;但是,你應該確保它使用標準的、Laravel 提供的 Session 基礎驗證服務 來驗證使用者。通常,這意味著使用web驗證 Guard。
保護路由 (Protecting Routes)
為了保護路由,使所有傳入的請求都必須經過驗證,你應該將 sanctum 驗證 Guard 附加到 routes/api.php 檔案中的 API 路由。此 Guard 將確保傳入的請求被驗證為來自 SPA 的有狀態驗證請求,或者如果請求來自第三方,則包含有效的 API Token 標頭:
use Illuminate\Http\Request;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
授權私有廣播頻道 (Authorizing Private Broadcast Channels)
如果你的 SPA 需要驗證 私有 / 存在廣播頻道,你應該從應用程式的 bootstrap/app.php 檔案中包含的 withRouting 方法中移除 channels 項目。相反地,你應該呼叫 withBroadcasting 方法,以便你可以為應用程式的廣播路由指定正確的 Middleware:
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 時需要提供一個自訂的 Pusher authorizer。這允許你的應用程式設定 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);
});
},
};
},
});
行動應用程式驗證 (Mobile Application Authentication)
你也可以使用 Sanctum Token 來驗證你的行動應用程式對 API 的請求。驗證行動應用程式請求的過程類似於驗證第三方 API 請求;但是,發行 API Token 的方式略有不同。
發行 API Token (Issuing API Tokens)
首先,建立一個路由,該路由接受使用者的電子郵件 / 使用者名稱、密碼和裝置名稱,然後交換這些憑證以獲取新的 Sanctum Token。提供給此端點的「裝置名稱」僅供參考,可以是任何你想要的值。通常,裝置名稱值應該是使用者可以識別的名稱,例如「Nuno's iPhone 12」。
通常,你會從行動應用程式的「登入」畫面請求 Token 端點。端點將回傳純文字 API Token,然後可以將其儲存在行動裝置上並用於發出其他 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;
});
當行動應用程式使用 Token 向你的應用程式發出 API 請求時,應將 Token 包含在 Authorization 標頭中作為 Bearer Token。
[!NOTE] 為行動應用程式發行 Token 時,你也可以自由指定 Token 能力。
保護路由 (Protecting Routes)
如前所述,你可以透過將 sanctum 驗證 Guard 附加到路由來保護路由,使所有傳入的請求都必須經過驗證:
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
撤銷 Token (Revoking Tokens)
為了允許使用者撤銷發行給行動裝置的 API Token,你可以在 Web 應用程式 UI 的「帳戶設定」部分按名稱列出它們,並附上「撤銷」按鈕。當使用者點擊「撤銷」按鈕時,你可以從資料庫中刪除該 Token。請記住,你可以透過 Laravel\Sanctum\HasApiTokens Trait 提供的 tokens 關聯存取使用者的 API Token:
// Revoke all tokens...
$user->tokens()->delete();
// Revoke a specific token...
$user->tokens()->where('id', $tokenId)->delete();
測試 (Testing)
在測試時,可以使用 Sanctum::actingAs 方法來驗證使用者並指定應授予其 Token 的能力:
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();
}
如果你想授予 Token 所有能力,你應該在提供給 actingAs 方法的能力列表中包含 *:
Sanctum::actingAs(
User::factory()->create(),
['*']
);