laravel
10. 软件包
Laravel Sanctum

介绍

Laravel Sanctum (opens in a new tab) 为单页应用程序(SPAs)、移动应用程序以及简单的基于令牌的 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 应用程序存在于同一个仓库中,也可能是一个完全独立的仓库,例如使用 Next.js 或 Nuxt 创建的 SPA。

对于此功能,Sanctum 不使用任何类型的令牌。相反,Sanctum 使用 Laravel 内置的基于 Cookie 的会话认证服务。通常,Sanctum 利用 Laravel 的 web 认证守卫来实现此目的。这提供了 CSRF 保护、会话认证的好处,以及防止通过 XSS 泄露认证凭据。

当传入请求来自您自己的 SPA 前端时,Sanctum 才会尝试使用 Cookie 进行认证。当 Sanctum 检查传入的 HTTP 请求时,它将首先检查认证 Cookie,如果不存在,Sanctum 将然后检查 Authorization 标头以获取有效的 API 令牌。

[!注意]
仅将 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 方法指示 Sanctum 使用您的自定义模型。通常,您应该在应用程序的 AppServiceProvider 文件的 boot 方法中调用此方法:

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

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

API 令牌认证

[!注意]
您不应该使用 API 令牌来认证您自己的第一方 SPA。相反,使用 Sanctum 的内置 SPA 认证功能

颁发 API 令牌

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

要开始为用户颁发令牌,您的用户模型应使用 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 认证的传入请求时,您可以使用 tokenCan 方法确定令牌是否具有给定的能力:

if ($user->tokenCan('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关系从数据库中删除它们来“撤销”令牌:

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

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

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

令牌过期

默认情况下,Sanctum 令牌永远不会过期,只能通过撤销令牌使其失效。但是,如果您想为您的应用程序的 API 令牌配置过期时间,您可以通过在应用程序的sanctum配置文件中定义的expiration配置选项来实现。此配置选项定义了颁发的令牌被视为过期的分钟数:

'expiration' => 525600,

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

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

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

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]
如果您通过包含端口的 URL(127.0.0.1:8000)访问您的应用程序,您应该确保将端口号与域名一起包含。

Sanctum 中间件

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

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

CORS 和 Cookie

如果您在从在单独的子域名上执行的 SPA 与您的应用程序进行认证时遇到问题,您可能错误配置了您的 CORS(跨源资源共享)或会话 cookie 设置。

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

php artisan config:publish cors

接下来,您应该确保您的应用程序的 CORS 配置返回Access-Control-Allow-Credentials头,其值为True。这可以通过将您的应用程序的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。然后,在后续请求中,这个令牌应在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 令牌标头:

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' => ['提供的凭据不正确。'],
        ]);
    }

    return $user->createToken($request->device_name)->plainTextToken;
});

当移动应用程序使用令牌向您的应用程序发出 API 请求时,它应该在Authorization标头中将令牌作为Bearer令牌传递。

[!NOTE]
在为移动应用程序颁发令牌时,您也可以自由指定令牌能力

保护路由

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

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

撤销令牌

为了允许用户撤销颁发给移动设备的 API 令牌,您可以在您的 Web 应用程序的用户界面的“账户设置”部分中按名称列出它们,并附带一个“撤销”按钮。当用户点击“撤销”按钮时,您可以从数据库中删除该令牌。请记住,您可以通过Laravel\Sanctum\HasApiTokens特征提供的tokens关系访问用户的 API 令牌:

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

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

测试

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

use App\Models\User;
use Laravel\Sanctum\Sanctum;
 
test('任务列表可以被获取', 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(),
    ['*']
);