Skip to content
全部文档

Laravel Sanctum ​

简介 ​

Laravel Sanctum 为 SPA(单页应用)、移动应用以及简单的基于令牌的 API 提供轻量级认证系统。Sanctum 允许应用的每位用户为其账户生成多个 API 令牌。这些令牌可被授予能力 / 作用域,以限定令牌可执行的操作。

工作原理 ​

Laravel Sanctum 旨在解决两个独立问题。在深入库之前,我们先分别说明。

API 令牌 ​

首先,Sanctum 是一个简单的包,可在不引入 OAuth 复杂性的情况下向用户签发 API 令牌。该特性受 GitHub 等签发「个人访问令牌」的应用启发。例如,应用的「账户设置」中可有一个页面让用户生成 API 令牌。你可用 Sanctum 生成和管理这些令牌。它们通常过期时间很长(数年),但用户可随时手动撤销。

Laravel Sanctum 将用户 API 令牌存在一张数据库表中,并通过应包含有效 API 令牌的 Authorization 头认证传入的 HTTP 请求。

SPA 认证 ​

其次,Sanctum 为需要与 Laravel 驱动的 API 通信的单页应用(SPA)提供简单认证方式。这些 SPA 可与 Laravel 应用同仓库,也可是独立仓库,例如用 Next.js 或 Nuxt 创建的 SPA。

对此特性,Sanctum 不使用任何令牌,而是使用 Laravel 内置的基于 cookie 的 session 认证服务。通常利用 Laravel 的 web 认证 guard,从而获得 CSRF 保护、session 认证,并防止认证凭据经 XSS 泄露。

仅当请求来自你自己的 SPA 前端时,Sanctum 才会尝试用 cookie 认证。检查传入 HTTP 请求时,会先查找认证 cookie;若不存在,再检查 Authorization 头中的有效 API 令牌。

INFO

仅将 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 方法指示使用自定义模型。通常应在应用 AppServiceProvider 的 boot 方法中调用:

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}

API 令牌认证 ​

INFO

你不应使用 API 令牌来认证自有的第一方 SPA。请改用 Sanctum 内置的 SPA 认证功能。

签发 API 令牌 ​

Sanctum 允许签发可用于认证 API 请求的 API 令牌 / 个人访问令牌。使用 API 令牌发请求时,应在 Authorization 头中以 Bearer 令牌形式包含该令牌。

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

php
use Laravel\Sanctum\HasApiTokens;

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

可使用 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 trait 提供的 tokens Eloquent 关联访问用户的所有令牌:

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

令牌能力 ​

Sanctum 允许为令牌分配「abilities」。Abilities 的作用类似 OAuth 的「scopes」。可将字符串 abilities 数组作为 createToken 的第二个参数传入:

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

处理经 Sanctum 认证的传入请求时,可用 tokenCan 或 tokenCant 判断令牌是否具备某能力:

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

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

令牌能力中间件 ​

Sanctum 还包含两个中间件,用于验证传入请求是否由已授予指定能力的令牌认证。开始时请在应用的 bootstrap/app.php 中定义以下中间件别名:

php
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 中间件分配给路由,以验证令牌具备列出的全部能力:

php
Route::get('/orders', function () {
    // Token has both "check-status" and "place-orders" abilities...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);

可将 ability 中间件分配给路由,以验证令牌具备列出能力中的至少一个:

php
Route::get('/orders', function () {
    // Token has the "check-status" or "place-orders" ability...
})->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')

乍看之下,对第一方 UI 发起的请求让 tokenCan 始终返回 true 似乎奇怪;但这样可以始终假设存在可经 tokenCan 检查的 API 令牌。采取该方式后,可在授权策略中始终调用 tokenCan,而无需担心请求来自应用 UI 还是 API 的第三方消费者。

保护路由 ​

为保护路由使所有传入请求必须经过认证,应在 routes/web.php 与 routes/api.php 中为受保护路由附加 sanctum 认证 guard。该 guard 会确保请求要么是有状态的 cookie 认证请求,要么(若来自第三方)包含有效的 API 令牌头。

你可能疑惑为何建议用 sanctum guard 认证 routes/web.php 中的路由。请记住,Sanctum 会先尝试用 Laravel 典型的 session 认证 cookie;若不存在,再尝试用请求 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,

若要为每个令牌单独指定过期时间,可将过期时间作为 createToken 的第三个参数传入:

php
return $user->createToken(
    'token-name', ['*'], now()->plus(weeks: 1)
)->plainTextToken;

若已为应用配置了令牌过期时间,你可能还希望调度任务以清理过期令牌。幸好 Sanctum 提供了 sanctum:prune-expired Artisan 命令来完成此事。例如,你可以配置定时任务,删除已过期至少 24 小时的全部过期令牌数据库记录:

php
use Illuminate\Support\Facades\Schedule;

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

SPA 认证 ​

Sanctum 也为需要与 Laravel 驱动的 API 通信的单页应用(SPA)提供简单认证方式。这些 SPA 可与 Laravel 应用同仓库,也可是独立仓库。

对此特性,Sanctum 不使用任何令牌,而是使用 Laravel 内置的基于 cookie 的 session 认证,从而获得 CSRF 保护、session 认证,并防止认证凭据经 XSS 泄露。

WARNING

为进行认证,SPA 与 API 必须共享同一顶级域名,但可位于不同子域名。此外,应确保请求发送 Accept: application/json 头,以及 Referer 或 Origin 头之一。

配置 ​

配置第一方域名 ​

首先应配置 SPA 发起请求的域名,可在 sanctum 配置文件中使用 stateful 选项。该设置决定哪些域名在向 API 发请求时使用 Laravel session cookie 保持「有状态」认证。

为帮助配置第一方有状态域名,Sanctum 提供两个可在配置中使用的辅助函数。Sanctum::currentApplicationUrlWithPort() 会从 APP_URL 返回当前应用 URL;Sanctum::currentRequestHost() 会向有状态域名列表注入占位符,运行时替换为当前请求的 host,从而使同域名请求都被视为有状态。

WARNING

若通过包含端口的 URL(127.0.0.1:8000)访问应用,应确保域名中包含端口号。

Sanctum 中间件 ​

接着应指示 Laravel:来自 SPA 的请求可用 Laravel session cookie 认证,同时仍允许第三方或移动应用用 API 令牌认证。可在 bootstrap/app.php 中调用 statefulApi 中间件方法轻松完成:

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

CORS 与 Cookie ​

若在独立子域名上运行的 SPA 认证应用时遇到问题,很可能是 CORS(跨域资源共享)或 session cookie 配置有误。

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

shell
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 客户端上做等效配置:

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

最后,应确保应用的 session cookie 域名配置支持根域名的任意子域名。可在 config/session.php 中为域名加上前导 .:

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

认证 ​

CSRF 保护 ​

要认证 SPA,SPA 的「登录」页应先向 /sanctum/csrf-cookie 端点发请求,以初始化应用的 CSRF 保护:

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

该请求期间,Laravel 会设置包含当前 CSRF 令牌的 XSRF-TOKEN cookie。随后应将该令牌 URL 解码,并在后续请求的 X-XSRF-TOKEN 头中传递;Axios、Angular HttpClient 等 HTTP 客户端库通常会自动完成。若你的 JavaScript HTTP 库不会设置,则需手动将 X-XSRF-TOKEN 设为该路由设置的 XSRF-TOKEN cookie 经 URL 解码后的值。

登录 ​

初始化 CSRF 保护后,应向 Laravel 应用的 /login 路由发起 POST 请求。该 /login 路由可手动实现,或使用 Laravel Fortify 这类无界面认证包。

登录成功后即完成认证,后续请求会通过 Laravel 发给客户端的 session cookie 自动认证。此外,由于应用已请求过 /sanctum/csrf-cookie,只要 JavaScript HTTP 客户端在 X-XSRF-TOKEN 头中发送 XSRF-TOKEN cookie 的值,后续请求应自动获得 CSRF 保护。

当然,若用户因长时间无活动导致 session 过期,后续请求可能收到 401 或 419 HTTP 错误。此时应将用户重定向到 SPA 的登录页。

WARNING

你可以自行编写 /login 端点;但应确保使用 Laravel 提供的标准、基于会话的认证服务来认证用户。通常这意味着使用 web 认证守卫。

保护路由 ​

为保护路由使所有传入请求必须经过认证,应在 routes/api.php 中为 API 路由附加 sanctum 认证 guard。该 guard 会确保请求要么是来自 SPA 的有状态认证请求,要么(若来自第三方)包含有效的 API 令牌头:

php
use Illuminate\Http\Request;

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

授权私有广播频道 ​

若 SPA 需要认证私有 / 存在广播频道,应从应用 bootstrap/app.php 中的 withRouting 方法移除 channels 条目。改为调用 withBroadcasting,以便为应用的广播路由指定正确的中间件:

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 authorizer。这样应用可将 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 令牌。传给该端点的「设备名」仅供参考,可为任意值。通常应是用户能识别的名称,例如「Nuno's iPhone 17」。

通常从移动应用的「登录」界面请求该令牌端点。端点会返回明文 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 令牌形式传递。

INFO

为移动应用签发令牌时,也可自由指定令牌能力。

保护路由 ​

如前所述,可通过为路由附加 sanctum 认证 guard,使所有传入请求必须经过认证:

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

撤销令牌 ​

为允许用户撤销发给移动设备的 API 令牌,可在 Web 应用 UI 的「账户设置」中按名称列出它们,并提供「撤销」按钮。用户点击后即可从数据库删除令牌。可通过 Laravel\Sanctum\HasApiTokens trait 提供的 tokens 关联访问用户的 API 令牌:

php
// Revoke all tokens...
$user->tokens()->delete();

// Revoke a specific token...
$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(),
    ['*']
);