Skip to content

Laravel Sanctum

介绍

Laravel Sanctum 为 SPA(单页应用程序)、移动应用程序和简单的基于令牌的 API 提供轻量级身份验证系统。Sanctum 允许您应用程序的每个用户为其帐户生成多个 API 令牌。可以向这些令牌授予能力/范围,以指定允许令牌执行哪些操作。

它是如何工作的

Laravel Sanctum 的存在是为了解决两个独立的问题。在深入研究库之前,让我们先讨论一下。

API Tokens

首先,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 令牌。

NOTE

仅将 Sanctum 用于 API 令牌身份验证或仅用于 SPA 身份验证是完全可以的。仅仅因为您使用 Sanctum 并不意味着您需要使用它提供的两个功能。

安装

您可以通过 install:api Artisan 命令安装 Laravel Sanctum:

shell
php artisan install:api

接下来,如果您计划使用 Sanctum 对 SPA 进行身份验证,请参阅本文档的 SPA 身份验证部分。

配置

覆盖默认模型

虽然通常不是必需的,但您可以自由扩展 Sanctum 内部使用的 PersonalAccessToken 模型:

php
    use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

    class PersonalAccessToken extends SanctumPersonalAccessToken
    {
        // ...
    }

然后,您可以通过 Sanctum 提供的 usePersonalAccessTokenModel 方法指示 Sanctum 使用您的自定义模型。通常,您应该在应用程序的 AppServiceProvider 文件的 boot 方法中调用此方法:

php
    use App\Models\Sanctum\PersonalAccessToken;
    use Laravel\Sanctum\Sanctum;

    /**
     * 引导任何应用程序服务
     */
    public function boot(): void
    {
        Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
    }

API Token 鉴权

NOTE

您不应使用 API 令牌来验证您自己的第一方 SPA。请改用 Sanctum 的内置 SPA 身份验证功能

颁发 API 令牌

Sanctum 允许您颁发 API 令牌/个人访问令牌,这些令牌可用于验证对应用程序的 API 请求。使用 API 令牌发出请求时,令牌应作为 Bearer 令牌包含在 Authorization 标头中。

要开始为用户颁发令牌,您的 User 模型应使用 Laravel\Sanctum\HasApiTokens trait:

php
    use Laravel\Sanctum\HasApiTokens;

    class User extends Authenticatable
    {
        use HasApiTokens, HasFactory, Notifiable;
    }

要颁发令牌,您可以使用 createToken 方法。createToken 方法返回一个 Laravel\Sanctum\NewAccessToken 实例。API 令牌在存储到数据库之前使用 SHA-256 哈希进行哈希处理,但您可以使用 NewAccessToken 实例的 plainTextToken 属性访问令牌的纯文本值。您应该在创建令牌后立即向用户显示此值:

php
    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 关系访问用户的所有令牌:

php
    foreach ($user->tokens as $token) {
        // ...
    }

令牌能力

Sanctum 允许您为token分配“能力”。Abilities 的用途与 OAuth 的 “scopes” 类似。你可以将字符串 abilities 数组作为第二个参数传递给 createToken 方法:

php
    return $user->createToken('token-name', ['server:update'])->plainTextToken;

在处理由 Sanctum 验证的传入请求时,您可以使用 tokenCan 方法确定令牌是否具有给定的能力:

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

Token Ability 中间件

Sanctum 还包括两个中间件,可用于验证传入请求是否使用已授予给定能力的令牌进行身份验证。首先,在应用程序的 bootstrap/app.php 文件中定义以下中间件别名:

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 中间件分配给路由,以验证传入请求的 token 是否具有列出的所有 abilities:

php
    Route::get('/orders', function () {
        // Token 同时具有 “check-status” 和 “place-orders” 功能...
    })->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);

可以将 ability middleware 分配给路由,以验证传入请求的 token 是否至少具有列出的 ability 之一:

php
    Route::get('/orders', function () {
        // Token 具有 “check-status” 或 “place-orders” 能力...
    })->middleware(['auth:sanctum', 'ability:check-status,place-orders']);

第一方 UI 发起的请求

为方便起见,如果传入的经过身份验证的请求来自您的第一方 SPA,并且您使用的是 Sanctum 的内置 SPA 身份验证,则 tokenCan 方法将始终返回 true

但是,这并不一定意味着您的应用程序必须允许用户执行该操作。通常,应用程序的授权策略将确定是否已向令牌授予执行功能的权限,并检查是否应允许用户实例本身执行操作。

例如,如果我们想象一个管理服务器的应用程序,这可能意味着检查令牌是否有权更新服务器**,以及**服务器是否属于用户:

php
return $request->user()->id === $server->user_id &&
       $request->user()->tokenCan('server:update')

起初,允许调用 tokenCan 方法并始终为第一方 UI 发起的请求返回 true 似乎很奇怪;但是,能够始终假设 API 令牌可用并且可以通过 tokenCan 方法进行检查,这很方便。通过采用此方法,您始终可以在应用程序的授权策略中调用 tokenCan 方法,而无需担心请求是从应用程序的 UI 触发的,还是由 API 的第三方使用者之一发起的。

路由保护

要保护路由,以便所有传入请求都必须进行身份验证,您应该在 routes/web.phproutes/api.php 路由文件中将 sanctum 身份验证守卫附加到受保护的路由。此防护将确保传入请求被认证为有状态的 cookie 认证请求,或者如果请求来自第三方,则包含有效的 API 令牌标头。

您可能想知道为什么我们建议您使用 sanctum guard 对应用程序的 routes/web.php 文件中的路由进行身份验证。请记住,Sanctum 将首先尝试使用 Laravel 的典型会话身份验证 cookie 来验证传入的请求。如果该 Cookie 不存在,则 Sanctum 将尝试使用请求的 Authorization 标头中的令牌对请求进行身份验证。此外,使用 Sanctum 对所有请求进行身份验证可确保我们始终可以在当前经过身份验证的用户实例上调用 tokenCan 方法:

php
    use Illuminate\Http\Request;

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

吊销令牌

您可以通过使用 Laravel\Sanctum\HasApiTokens trait 提供的 tokens 关系从数据库中删除令牌来 “撤销” 令牌:

php
    // 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 配置选项来实现。此配置选项定义颁发的令牌被视为过期之前的分钟数:

php
'expiration' => 525600,

如果您想单独指定每个 Token 的过期时间,您可以通过将过期时间作为 createToken 方法的第三个参数来实现:

php
return $user->createToken(
    'token-name', ['*'], now()->addWeek()
)->plainTextToken;

如果您已为应用程序配置了令牌过期时间,则可能还希望安排一个任务来删除应用程序的过期令牌。值得庆幸的是,Sanctum 包含一个 sanctum:prune 过期的 Artisan 命令,您可以使用该命令来完成此操作。例如,您可以将计划任务配置为删除所有已过期至少 24 小时的过期令牌数据库记录:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('sanctum:prune-expired --hours=24')->daily();

SPA 身份验证

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

如果您通过包含端口 (127.0.0.1:8000) 的 URL 访问应用程序,则应确保在域中包含端口号。

Sanctum 中间件

接下来,您应该指示 Laravel,来自 SPA 的传入请求可以使用 Laravel 的会话 Cookie 进行身份验证,同时仍允许来自第三方或移动应用程序的请求使用 API 令牌进行身份验证。这可以通过在应用程序的 bootstrap/app.php 文件中调用 statefulApi 中间件方法轻松实现:

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

如果您在从单独子域上执行的 SPA 对应用程序进行身份验证时遇到问题,则可能是您错误地配置了 CORS(跨域资源共享)或会话 Cookie 设置。

默认情况下,config/cors.php 配置文件不会发布。如果你需要自定义 Laravel 的 CORS 选项,你应该使用 config:publish Artisan 命令发布完整的 cors 配置文件:

bash
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 客户端上执行等效的配置:

js
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;

最后,您应该确保应用程序的会话 cookie 域配置支持根域的任何子域。您可以通过在应用程序的 config/session.php 配置文件中为域添加前导 .来实现此目的:

php
    'domain' => '.domain.com',

认证

CSRF 保护

要验证您的 SPA,您的 SPA 的 “login” 页面应首先向 /sanctum/csrf-cookie 端点发出请求,以初始化应用程序的 CSRF 保护:

js
axios.get('/sanctum/csrf-cookie').then(response => {
    // 登录...
});

在此请求期间,Laravel 将设置一个包含当前 CSRF 令牌的 XSRF-TOKEN cookie。然后,这个令牌应该在后续请求的 X-XSRF-TOKEN 头中传递,一些 HTTP 客户端库,如 Axios 和 Angular HttpClient,会自动为你完成。如果您的 JavaScript HTTP 库没有为您设置该值,则需要手动设置 X-XSRF-TOKEN 标头,以匹配此路由设置的 XSRF-TOKEN Cookie 的值。

登录

初始化 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 的登录页面。

WARNING

您可以自由编写自己的 /login 终端节点;但是,您应该确保它使用 Laravel 提供的基于会话的标准身份验证服务对用户进行身份验证。通常,这意味着使用 Web 身份验证防护。

路由保护

要保护路由以便所有传入请求都必须进行身份验证,您应该在 routes/api.php 文件中将 sanctum 身份验证守卫附加到 API 路由。此防护将确保传入请求作为来自 SPA 的有状态身份验证请求进行身份验证,或者如果请求来自第三方,则包含有效的 API 令牌标头:

php
    use Illuminate\Http\Request;

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

授权私网频道

如果您的 SPA 需要使用 private / presence 广播通道进行身份验证,则应从应用程序的 bootstrap/app.php 文件中包含的 withRouting 方法中删除 channels 条目。相反,您应该调用 withBroadcasting 方法,以便为应用程序的广播路由指定正确的 middleware:

php
    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 授权方。这允许您的应用程序将 Pusher 配置为使用为跨域请求正确配置的 axios 实例:

js
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 令牌。为此端点提供的 “device name” 仅供参考,可以是您希望的任何值。通常,设备名称值应该是用户能够识别的名称,例如“Nuno 的 iPhone 12”。

通常,您将从移动应用程序的 “login” 屏幕向令牌终端节点发出请求。端点将返回纯文本 API 令牌,该令牌随后可以存储在移动设备上并用于发出其他 API 请求:

php
    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 请求时,它应将 Authorization 标头中的令牌作为 Bearer 令牌传递。

NOTE

在为移动应用程序颁发令牌时,您还可以自由指定令牌功能

路由保护

如前所述,您可以保护路由,以便必须通过将 sanctum 身份验证守卫附加到路由来验证所有传入请求:

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

吊销令牌

要允许用户撤销颁发给移动设备的 API 令牌,您可以在 Web 应用程序 UI 的“帐户设置”部分按名称列出它们,并列出“撤销”按钮。当用户单击 “Revoke” 按钮时,您可以从数据库中删除 Token。请记住,您可以通过 Laravel\Sanctum\HasApiTokens trait 提供的 tokens 关系来访问用户的 API 令牌:

php
    // 撤销所有令牌...
    $user->tokens()->delete();

    // 撤销特定令牌...
    $user->tokens()->where('id', $tokenId)->delete();

测试

在测试时,可以使用 Sanctum::actingAs 方法对用户进行身份验证,并指定应向其令牌授予哪些能力:

php
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();
});
php
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 方法的技能列表中包含 *:

php
    Sanctum::actingAs(
        User::factory()->create(),
        ['*']
    );