Laravel Sanctum

 

介绍

Laravel Sanctum 提供了一个轻量级的认证系统,可用于 SPA(单页应用程序)、移动应用程序和基于简单令牌的 API。Sanctum 允许的应用程序中的每个用户为他们的账户生成多个 API 令牌。这些令牌可以被授予权限/范围,以指定令牌允许执行哪些操作。

工作原理

Laravel Sanctum 旨在解决两个不同的问题。在深入探讨该库之前,让我们先讨论一下每个问题。

API 令牌

首先,Sanctum 是一个简单的包,你可以使用它向你的用户发出 API 令牌,而无需 OAuth 的复杂性。这个功能受到 GitHub 和其他应用程序发出「访问令牌」的启发。例如,假如你的应用程序的「账户设置」有一个界面,用户可以在其中为他们的账户生成 API 令牌。你可以使用 Sanctum 生成和管理这些令牌。这些令牌通常具有非常长的过期时间(以年计),但用户可以随时手动撤销它们。

Laravel Sanctum 通过将用户 API 令牌存储在单个数据库表中,并通过应该包含有效 API 令牌的 Authorization 标头对传入的 HTTP 请求进行身份验证来提供此功能。

SPA 认证

第二个功能,Sanctum 存在的目的是为需要与 Laravel 支持的 API 通信的单页应用程序 (SPAs) 提供一种简单的身份验证方式。这些 SPAs 可能存在于与 Laravel 应用程序相同的存储库中,也可能是一个完全独立的存储库,例如使用 Vue CLI 创建的 SPA 或 Next.js 应用程序。

对于此功能,Sanctum 不使用任何类型的令牌。相反,Sanctum 使用 Laravel 内置基于 cookie 的会话身份验证服务。通常,Sanctum 使用 Laravel 的 web 认证保护方式实现这一点。这提供了 CSRF 保护、会话身份验证以及防止通过 XSS 泄漏身份验证凭据的好处。

只有在传入请求来自你自己的 SPA 前端时,Sanctum 才会尝试使用 cookies 进行身份验证。当 Sanctum 检查传入的 HTTP 请求时,它首先会检查身份验证 cookie,如果不存在,则 Sanctum 会检查 Authorization 标头是否包含有效的 API 令牌。

注意
完全可以只使用 Sanctum 进行 API 令牌身份验证或只使用 Sanctuary 进行 SPA 身份验证。仅因为你使用 Sanctum 并不意味着你必须使用它提供的两个功能。

安装

注意
最近的 Laravel 版本已经包括 Laravel Sanctum。但如果你的应用程序的 composer.json 文件不包括 laravel/sanctum,你可以遵循下面的安装说明。

你可以通过 Composer 包管理器安装 Laravel Sanctum:

composer require laravel/sanctum

接下来,你应该使用 vendor:publish Artisan 命令发布 Sanctum 配置文件和迁移文件。sanctum 配置文件将被放置在你的应用程序的 config 目录中:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

最后,你应该运行数据库迁移。Sanctum 会创建一个数据库表来存储 API 令牌:

php artisan migrate

接下来,如果你打算使用 Sanctum 来对 SPA 单页应用程序进行认证,则应该将 Sanctum 的中间件添加到你的应用程序的 app/Http/Kernel.php 文件中的 api 中间件组中:

'api' => [
   \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
   \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
   \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

自定义迁移

如果你不打算使用 Sanctum 的默认迁移文件,则应该在 App\Providers\AppServiceProvider 类的 register 方法中调用 Sanctum::ignoreMigrations 方法。你可以通过执行以下命令导出默认的迁移文件:php artisan vendor:publish --tag=sanctum-migrations

配置

覆盖默认模型

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

use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
{
    // ...
}

然后,你可以通过 Sanctum 提供的 usePersonalAccessTokenModel 方法来指示 Sanctum 使用你的自定义模型。通常,你应该在一个应用程序的服务提供者的 boot 方法中调用此方法:

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

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

API 令牌认证

注意
你不应该使用 API 令牌来认证你自己的第一方单页应用程序。而应该使用 Sanctum 内置的 SPA 身份验证功能

发行 API 令牌

Sanctum 允许你发行 API 令牌/个人访问令牌,可用于对你的应用程序的 API 请求进行身份验证。使用 API 令牌发出请求时,应将令牌作为 Bearer 令牌包括在 Authorization 头中。

要开始为用户发行令牌,你的用户模型应该使用 Laravel\Sanctum\HasApiTokens trait:

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

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

令牌能力

Sanctum 允许你为令牌分配「能力」 。能力的作用类似于 OAuth 的「Scope」 。你可以将一个字符串能力数组作为 createToken 方法的第二个参数传递:

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

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

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

令牌能力中间件

Sanctum 还包括两个中间件,可用于验证传入的请求是否使用授予了给定能力的令牌进行了身份验证。首先,请将以下中间件添加到应用程序的 app/Http/Kernel.php 文件的 $middlewareAliases 属性中:

'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\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 的第三方使用者发起的。

保护路由

为了保护路由,使所有入站请求必须进行身份验证,你应该在你的 routes/web.phproutes/api.php 路由文件中,将 sanctum 认证守卫附加到受保护的路由上。如果该请求来自第三方,该守卫将确保传入的请求经过身份验证,要么是具有状态的 Cookie 身份验证请求,要么是包含有效的 API 令牌标头的请求。

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

use Illuminate\Http\Request;

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

撤销令牌

你可以通过使用 Laravel\Sanctum\HasApiTokens trait 提供的 tokens 关系,从数据库中删除它们来达到「撤销」令牌的目的:

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

// 撤销用于验证当前请求的令牌...
$request->user()->currentAccessToken()->delete();

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

令牌有效期

默认情况下,Sanctum 令牌永不过期,并且只能通过撤销令牌进行无效化。但是,如果你想为你的应用程序 API 令牌配置过期时间,可以通过在应用程序的 sanctum 配置文件中定义的 expiration 配置选项进行配置。此配置选项定义发放的令牌被视为过期之前的分钟数:

// 365天后过期
'expiration' => 525600,

如果你已为应用程序配置了令牌过期时间,你可能还希望任务调度来删除应用程序过期的令牌。幸运的是,Sanctum 包括一个 sanctum:prune-expired Artisan 命令,你可以使用它来完成此操作。例如,你可以配置计划任务来删除所有过期至少24小时的令牌数据库记录:

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

SPA 身份验证

Sanctum 还提供一种简单的方法来验证需要与 Laravel API 通信的单页面应用程序(SPA)。这些 SPA 可能存在于与你的 Laravel 应用程序相同的存储库中,也可能是一个完全独立的存储库。

对于此功能,Sanctum 不使用任何类型的令牌。相反,Sanctum 使用 Laravel 内置的基于 cookie 的 session 身份验证服务。此身份验证方法提供了 CSRF 保护、session 身份验证以及防止身份验证凭据通过 XSS 泄漏的好处。

警告
为了进行身份验证,你的 SPA 和 API 必须共享相同的顶级域。但是,它们可以放置在不同的子域中。此外,你应该确保你的请求中发送 Accept: application/json 头文件。

配置

配置你的第一个域

首先,你应该通过 sanctum 配置文件中的 stateful 配置选项来配置你的 SPA 将从哪些域发出请求。此配置设置确定哪些域将在向你的 API 发送请求时使用 Laravel session cookie 维护「有状态的」身份验证。

警告
如果你通过包含端口的 URL(127.0.0.1:8000)访问应用程序,你应该确保在域名中包括端口号。

Sanctum 中间件

接下来,你应该将 Sanctum 中间件添加到你的 app/Http/Kernel.php 文件中的 api 中间件组中。此中间件负责确保来自你的 SPA 的传入请求可以使用 Laravel 会话 cookie 进行身份验证,同时仍允许来自第三方或移动应用程序使用 API 令牌进行身份验证:

'api' => [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
   \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
   \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

CORS 和 Cookies

如果你无法从执行在单独子域上的 SPA 中进行应用程序身份验证的话,你可能已错误配置了 CORS(跨域资源共享)或会话 cookie 设置。

你应该确保你的应用程序的 CORS 配置返回的 Access-Control-Allow-Credentials 请求头的值为 True 。这可以通过在应用程序的 config/cors.php 配置文件中设置 supports_credentials 选项为 true 来完成。

此外,你应该在应用程序的全局 axios 实例中启用 withCredentials 选项。通常,这应该在你的 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 => {
    // Login...
});

在此请求期间,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 的登录页面。

警告
你可以自己编写 /login 端点;但是,你应该确保使用 Laravel 提供的标准基于会话的身份验证服务来验证用户。通常,这意味着使用 web 身份验证 Guard。

保护路由

为了保护路由,以便所有传入的请求必须进行身份验证,你应该将 sanctum 身份验证 guard 附加到 routes/api.php 文件中的 API 路由上。这个 guard 将确保传入的请求被验证为来自你的 SPA 的有状态身份验证请求,或者如果请求来自第三方,则包含有效的 API 令牌标头:

use Illuminate\Http\Request;

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

授权私有广播频道

如果你的 SPA 需要对私有/存在 broadcast 频道进行身份验证,你应该在 routes/api.php 文件中调用 Broadcast::routes 方法:

Broadcast::routes(['middleware' => ['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’s 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 标头中传递。

注意
当为移动应用程序发布令牌时,你可以自由指定令牌权限

路由保护

如之前所述,你可以通过使用 sanctum 认证守卫附加到路由上来保护路由,以便所有传入请求都必须进行身份验证:

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

撤销令牌

为了允许用户撤销发放给移动设备的 API 令牌,你可以在 Web 应用程序 UI 的 「帐户设置」部分中按名称列出它们,并提供一个「撤销」按钮。当用户点击「撤销」按钮时,你可以从数据库中删除令牌。请记住,你可以通过 Laravel\Sanctum\HasApiTokens 特性提供的 tokens 关系访问用户的 API 令牌:

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

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

测试

在测试时,Sanctum::actingAs 方法可用于验证用户并指定为其令牌授予哪些能力:

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(),
    ['*']
);