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:
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 令牌认证
INFO
你不应使用 API 令牌来认证自有的第一方 SPA。请改用 Sanctum 内置的 SPA 认证功能。
签发 API 令牌
Sanctum 允许签发可用于认证 API 请求的 API 令牌 / 个人访问令牌。使用 API 令牌发请求时,应在 Authorization 头中以 Bearer 令牌形式包含该令牌。
要开始为用户签发令牌,User 模型应使用 Laravel\Sanctum\HasApiTokens trait:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
可使用 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 允许为令牌分配「abilities」。Abilities 的作用类似 OAuth 的「scopes」。可将字符串 abilities 数组作为 createToken 的第二个参数传入:
return $user->createToken('token-name', ['server:update'])->plainTextToken;
处理经 Sanctum 认证的传入请求时,可用 tokenCan 或 tokenCant 判断令牌是否具备某能力:
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 () {
// Token has both "check-status" and "place-orders" abilities...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);
可将 ability 中间件分配给路由,以验证令牌具备列出能力中的至少一个:
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。
不过,这并不意味着应用必须允许用户执行该操作。通常,应用的授权策略还会判断令牌是否被授予执行这些能力的权限,以及该用户实例本身是否应被允许执行该操作。
例如,若想象一个管理服务器的应用,可能意味着检查令牌有权更新服务器,并且该服务器属于该用户:
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:
use Illuminate\Http\Request;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
撤销令牌
可通过 Laravel\Sanctum\HasApiTokens trait 提供的 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();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 保持「有状态」认证。
WARNING
若通过包含端口的 URL(127.0.0.1:8000)访问应用,应确保域名中包含端口号。
Sanctum 中间件
接着应指示 Laravel:来自 SPA 的请求可用 Laravel session cookie 认证,同时仍允许第三方或移动应用用 API 令牌认证。可在 bootstrap/app.php 中调用 statefulApi 中间件方法轻松完成:
->withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi();
})
CORS 与 Cookie
若在独立子域名上运行的 SPA 认证应用时遇到问题,很可能是 CORS(跨域资源共享)或 session cookie 配置有误。
config/cors.php 默认不会发布。若需自定义 Laravel 的 CORS 选项,应使用 config:publish Artisan 命令发布完整的 cors 配置文件:
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 客户端上做等效配置:
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;最后,应确保应用的 session 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。随后应将该令牌 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 令牌头:
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's iPhone 17」。
通常从移动应用的「登录」界面请求该令牌端点。端点会返回明文 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 请求时,应在 Authorization 头中以 Bearer 令牌形式传递。
INFO
为移动应用签发令牌时,也可自由指定令牌能力。
保护路由
如前所述,可通过为路由附加 sanctum 认证 guard,使所有传入请求必须经过认证:
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
撤销令牌
为允许用户撤销发给移动设备的 API 令牌,可在 Web 应用 UI 的「账户设置」中按名称列出它们,并提供「撤销」按钮。用户点击后即可从数据库删除令牌。可通过 Laravel\Sanctum\HasApiTokens trait 提供的 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(),
['*']
);