Skip to content

Latest commit

 

History

History
507 lines (361 loc) · 22.4 KB

sanctum.md

File metadata and controls

507 lines (361 loc) · 22.4 KB

Laravel Sanctum

簡介

Laravel Sanctum 為 SPA(單頁應用程式)、行動應用程式和簡單基於權杖的 API 提供了一個輕量級的認證系統。Sanctum 允許您的應用程式的每個使用者為其帳戶生成多個 API 權杖。這些權杖可以被授予權限/範圍,指定權杖允許執行的操作。

運作方式

Laravel Sanctum 的存在是為了解決兩個不同的問題。在深入研究庫之前,讓我們討論每個問題。

API 權杖

首先,Sanctum 是一個簡單的套件,您可以使用它向用戶發行 API 權杖,而無需 OAuth 的複雜性。此功能受 GitHub 和其他發行「個人訪問權杖」的應用程式啟發。例如,想像您的應用程式的「帳戶設定」有一個畫面,用戶可以為其帳戶生成 API 權杖。您可以使用 Sanctum 來生成和管理這些權杖。這些權杖通常具有非常長的到期時間(年),但可以隨時由用戶手動撤銷。

Laravel Sanctum 通過將使用者 API 權杖存儲在單個資料庫表中並通過 Authorization 標頭對傳入的 HTTP 請求進行身份驗證來提供此功能,該標頭應包含有效的 API 權杖。

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 權杖。

Note

只使用 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 提供的 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 權杖驗證

Note

您不應該使用 API 權杖來驗證您自己的第一方單頁應用程式。請改用 Sanctum 內建的 SPA 驗證功能

發行 API 權杖

Sanctum 允許您發行 API 權杖 / 個人存取權杖,用於驗證對您應用程式的 API 請求。在使用 API 權杖進行請求時,應將該權杖包含在 Authorization 標頭中,作為 Bearer 權杖。

要開始為使用者發行權杖,您的 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 驗證的傳入請求時,您可以使用 tokenCantokenCant 方法來確定權杖是否具有特定權限:

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

if ($user->tokenCant('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 () {
    // 標記同時具有 "check-status" 和 "place-orders" 權限...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);

ability 中介層可分配給路由,以驗證傳入請求的標記是否至少具有列出的一個權限:

Route::get('/orders', function () {
    // 標記具有 "check-status" 或 "place-orders" 權限...
})->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 的第三方消費者之一發起的。

保護路由

為了保護路由,使所有傳入的請求都必須通過身份驗證,您應該將 sanctum 身份驗證守衛附加到您的受保護路由中,在您的 routes/web.phproutes/api.php 路由文件中。此守衛將確保傳入的請求是作為有狀態的、使用 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();

單頁應用程式認證

Sanctum 也存在為提供一個簡單的方法來對需要與 Laravel 驅動的 API 通信的單頁應用程式(SPA)進行身份驗證。這些 SPA 可能存在於與您的 Laravel 應用程式相同的存儲庫中,也可能是一個完全獨立的存儲庫。

對於這個功能,Sanctum 不使用任何類型的標記。相反,Sanctum 使用 Laravel 內建的基於 cookie 的會話身份驗證服務。這種身份驗證方法提供了 CSRF 保護、會話身份驗證的好處,以及防止通過 XSS 洩漏身份驗證憑證的保護。

Warning

為了進行身份驗證,您的 SPA 和 API 必須共享相同的頂級域。但是,它們可以放置在不同的子域上。此外,您應確保您發送 Accept: application/json 標頭以及您的請求中的 RefererOrigin 標頭。

配置

配置您的第一方域

首先,您應該配置您的 SPA 將從哪些域進行請求。您可以使用您的 sanctum 配置檔案中的 stateful 配置選項來配置這些域。此配置設置確定哪些域將在向您的 API 發送請求時使用 Laravel 會話 cookie 進行“有狀態”的身份驗證。

Warning

如果您通過包含端口號的 URL(127.0.0.1:8000)訪問應用程式,您應確保將端口號與域名一起包含。

Sanctum Middleware

接下來,您應該指示 Laravel,您的 SPA 發出的請求可以使用 Laravel 的會話 Cookie 進行身份驗證,同時仍允許第三方或移動應用程式使用 API 令牌進行身份驗證。這可以通過在應用程式的 bootstrap/app.php 檔案中調用 statefulApi 中介層方法輕鬆完成:

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

CORS 和 Cookies

如果您在從在不同子域上執行的 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 => {
    // 登入...
});

在此請求期間,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 路由發出請求,後續請求應該自動接收 CSRF 保護,只要您的 JavaScript HTTP 客戶端在 X-XSRF-TOKEN 標頭中發送 XSRF-TOKEN Cookie 的值。

當然,如果由於活動不足而導致用戶會話過期,對 Laravel 應用程式的後續請求可能會收到 401 或 419 HTTP 錯誤響應。在這種情況下,您應該將用戶重定向到您的 SPA 的登入頁面。

Warning

您可以自由編寫自己的 /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 時提供自定義的 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);
                });
            }
        };
    },
})

行動應用程式身分驗證

您也可以使用 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 標頭傳遞。

Note

當為行動應用程式發出令牌時,您也可以自由指定 令牌權限

保護路由

如先前所述,您可以保護路由,以便所有傳入請求必須通過將 sanctum 認證守衛附加到路由來進行身份驗證:

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

撤銷令牌

為了讓用戶撤銷發放給行動設備的 API 令牌,您可以按名稱列出它們,並在網頁應用程式的 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(),
    ['*']
);