基于 Laravel Sanctum 提供 SPA 认证解决方案
简介
Laravel Sanctum 为 SPA(Single Page Application,单页面应用)、移动 App 以及基于令牌的简单 API 提供了一个轻量级的认证系统。Sanctum 允许为应用的每个用户账户生成多个 API 令牌,这些令牌可用于授予权限/作用域来指定对应令牌允许执行的操作。
工作原理
Laravel Sanctum 的存在是为了解决两个问题。
API 令牌
首先,通过这个扩展包可以在不引入 OAuth 的情况下为用户颁发 API 令牌,OAuth 那一套流程太复杂了。这个特性主要源自 Github 访问令牌功能的启发,例如,假设应用的账户设置界面可用于为用户生成 API 令牌,你可以通过 Sanctum 来生成并管理这些令牌。这些令牌通常拥有很长的过期时间(以年计算),但是用户可以在任何时候手动清除该令牌。
Laravel Sanctum 通过保存用户 API 令牌到独立的数据表,然后当输入请求头 Authorization
包含有效 API 令牌时对其进行验证来实现 API 认证功能。
SPA 认证
其次,Sanctum 还提供了一套简单的机制对单页面应用(SPA)与 Laravel 后端 API 之间的通信进行认证。这些 SPA 可以和 Laravel 应用位于同一个代码仓库,也可以位于独立的代码仓库,比如通过 Vue CLI 创建的 SPA。
Sanctum 并没有使用任何形式的令牌实现认证。取而代之地,Sanctum 会使用 Laravel 内置的基于 Cookie + Session 的认证服务,这样一来,我们就可以充分利用 CSRF 保护、Session 认证、以及防止基于 XSS 的认证凭证泄漏。当然,Scanctum 只会对来自自己系统的 SPA 前端(根域名一致)请求进行基于 Cookie + Session 的认证。
注:将 Sanctum 只用于 API 令牌认证或者只用于 SPA 认证都是可以的,因为当你使用 Sanctum 时,并不意味着要求你必须使用这两个特性。
安装
通过 Composer 安装 Laravel Sanctum:
composer require laravel/sanctum
安装完成后,通过 vendor:publish
命令发布扩展包的配置和迁移文件,该命令会将 sanctum
配置文件存放到 config
目录下:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
最后,运行数据库迁移命令生成扩展包需要的数据表:
php artisan migrate
该命令会新创建一个 personal_access_tokens
表用于存放用户 API 认证信息:
接下来,如果你是为单页面应用提供认证的话,需要在 app/Http/Kernel.php
的 api
中间件分组中添加 Sanctum 中间件:
use Laravel\Airlock\Http\Middleware\EnsureFrontendRequestsAreStateful;
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
迁移自定义
如果你不准备使用 Sanctum 默认的迁移文件,可以在 AppServiceProvider
的 register
方法中调用 Sanctum::ignoreMigrations
方法。然后使用 php artisan vendor:publish --tag=sanctum-migrations
导出默认迁移文件进行自定义。
API 令牌认证
注:不要使用 API 令牌来认证自己的第一方 SPA,取而代之地,应该使用 Sanctum 内置的 SPA 认证。
颁发 API 令牌
Sanctum 允许你颁发 API 令牌或者个人访问令牌用于认证 API 请求,当使用 API 令牌发起请求时,该令牌应该以 Bearer
令牌形式包含在 Authorization
请求头中。
在为用户颁发令牌之前,需要在 User
模型类中使用 HasApiTokens
Trait:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}
要颁发令牌,可以使用 createToken
方法,该方法会返回一个 Laravel\Sanctum\NewAccessToken
实例。API 令牌在存放到数据库之前会使用 SHA-256 算法进行哈希,不过你可以使用 NewAccessToken
实例的 plainTextToken
属性来访问该令牌的纯文本格式值。你需要在令牌创建之后立即将其展示给用户:
$token = $user->createToken('token-name');
return $token->plainTextToken;
你可以通过 HasApiTokens
Trait 提供的 tokens
关联关系访问用户的所有令牌:
foreach ($user->tokens as $token) {
//
}
令牌权限
Sanctum 允许你为令牌分配权限,该功能和 OAuth 的作用域类似。你可以传递权限字符串数组作为 createToken
方法的第二个参数:
return $user->createToken('token-name', ['server:update'])->plainTextToken;
在使用 Sanctum 处理输入请求认证时,你可以使用 tokenCan
方法判断令牌是否具备给定权限:
if ($user->tokenCan('server:update')) {
//
}
注:为了方便,如果输入认证请求来自第一方 SPA 并且你正在使用 Sanctum 内置的 SPA 认证,则
tokenCan
方法总是返回true
。
保护路由
要保护路由确保所有输入请求必须经过认证,需要在 routes/api.php
路由文件中应用 sanctum
认证守卫到对应 API 路由。该守卫可以确保输入请求要么以来自 SPA 的状态认证请求方式认证,要么以请求头包含有效 API 令牌的第三方请求方式认证:
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
撤销令牌
你可以通过 HasApiTokens
Trait 提供的 tokens
关联关系删除数据库中的令牌来实现令牌「撤销」:
// 撤销所有令牌...
$user->tokens()->delete();
// 撤销用户当前令牌...
$request->user()->currentAccessToken()->delete();
// 撤销指定令牌...
$user->tokens()->where('id', $id)->delete();
SPA 认证
Sanctum 提供了一套简单的机制对单页面应用(SPA)与 Laravel 后端 API 之间的通信进行认证。这些 SPA 可以和 Laravel 应用位于同一个代码仓库,也可以位于独立的代码仓库,比如通过 Vue CLI 创建的 SPA。
Sanctum 并没有使用任何形式的令牌实现认证。取而代之地,Sanctum 会使用 Laravel 内置的基于 Cookie + Session 的认证服务,这样一来,我们就可以充分利用 CSRF 保护、Session 认证、以及防止基于 XSS 的认证凭证泄漏。当然,Scanctum 只会对来自自己系统的 SPA 前端(根域名一致)请求进行基于 Cookie + Session 的认证。
注:要实现这种认证,SPA 前端和后端 API 必须共享同样的顶级域名(子域名可以不同),比如都是
www.blog.test
,或者 SPA 前端是blog.test
,后端 API 是api.blog.test
。
配置
配置第一方域名
首先,你需要配置 SPA 发起请求的域名,这可以在 sanctum
配置文件的 stateful
配置项中完成。该配置设置可以决定哪些域名可以在发起 API 接口请求时使用 Laravel Session 认证维护认证状态:
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
注:如果你在通过包含端口(例如
127.0.0.1:8000
)的 URL 访问应用,需要确保在域名中包含了端口号。
Sanctum 中间件
接下来,你需要添加 Sanctum 中间件到 app/Http/Kernel.php
文件的 api
中间件分组。该中间件负责确保来自 SPA 的输入请求可以使用 Laravel Session 机制进行认证,同时也支持来自第三方或者移动应用的请求使用 API 令牌进行认证:
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
CORS & Cookies
如果你在处理来自独立子域名的 SPA 请求认证时遇到问题,可能是没有配置 CORS 或者 Session cookie 设置。
你需要确保应用的 CORS 配置会返回 Access-Control-Allow-Credentials
值为 True
的响应头,这可以通过在 cors
配置文件中将 supports_credentials
配置值设置为 true
来完成:
'supports_credentials' => true,
此外,你需要在全局 axios
实例中启用 withCredentials
选项,通常,这可以在 resources/js/bootstrap.js
文件中完成:
axios.defaults.withCredentials = true;
最后,你需要确保你的应用会话 Cookie 域名配置支持根域名的任意子域名,这可以通过在 session
配置文件中为 domain
配置值添加一个 .
前缀来完成(或者通过在 .env
中设置 SESSION_DOMAIN
):
'domain' => '.domain.com',
认证
要认证 SPA,SPA 的登录页面需要首先对 /sanctum/csrf-cookie
路由发起请求来初始化 CSRF 保护:
axios.get('/sanctum/csrf-cookie').then(response => {
// Login...
});
在请求期间 Laravel 会设置包含当前 CSRF 令牌的 XSRF-TOKEN
Cookie。该令牌在后续请求中会被 Axios 以及 Angular HttpClient 之类的 JavaScript 库自动设置到 X-XSRF-TOKEN
请求头中。
CSRF 保护初始化之后,需要向 /login
路由发起 POST
请求,/login
路由可以通过 laravel/jetstream
认证脚手架扩展包提供。
如果登录请求成功,则用户认证成功,针对 API 的后续请求序列会自动通过 Cookie + Session 机制进行认证。
注:你可以自由编写自己的
/login
接口实现,不过,需要确保使用标准的、Laravel 提供的、基于 Session 的认证服务进行认证。
保护路由
要保护路由确保所有输入请求必须经过认证,需要在 routes/api.php
路由文件中应用 sanctum
认证守卫到对应 API 路由。该守卫可以确保输入请求要么以来自 SPA 的状态认证请求方式认证,要么以请求头包含有效 API 令牌的第三方请求方式认证:
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
授权私有广播频道
如果你的 SPA 需要使用私有/存在广播频道进行认证,可以在 routes/api.php
文件中调用 Broadcast::routes
方法:
Broadcast::routes(['middleware' => ['auth:sanctum']]);
然后,为了让 Pusher 的授权请求可以成功,需要在初始化 Laravel Echo 时提供自定义的 Pusher authorizer
,这样一来,应用就可以配置 Pusher 使用已经配置过正确处理 CORS 请求的 axios
实例了:
window.Echo = new Echo({
broadcaster: "pusher",
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
encrypted: true,
key: process.env.MIX_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 令牌。API 端点会返回纯文本格式的 Sanctum 令牌,然后将其存储到移动设备用于后续发起 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
令牌中。
注:为移动应用颁发令牌时,还可以指定令牌权限。
保护路由
正如前面所介绍的,你可以通过添加 sanctum
认证守卫到路由来确保所有输入请求必须经过认证,从而实现路由保护。通常,你可以添加这个守卫到定义在 routes/api.php
文件的路由上:
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
撤销令牌
为了允许用户撤销颁发给移动设备的 API 令牌,你可以在一个「账户设置」页面通过名称来列举它们,并附上一个「撤销」按钮。当用户点击「撤销」按钮时,你可以从数据库删除对应令牌。你可以通过 HasApiTokens
trait 提供的 tokens
关联关系来访问用户的所有 API 令牌:
// Revoke all tokens...
$user->tokens()->delete();
// Revoke a specific token...
$user->tokens()->where('id', $id)->delete();
测试
测试期间,可以使用 Sanctum::actingAs
方法来认证用户并指定分配给对应令牌的权限:
use App\Models\User;
use Laravel\Sanctum\Sanctum;
public function test_task_list_can_be_retrieved()
{
Sanctum::actingAs(
User::factory()->create(),
['view-tasks']
);
$response = $this->get('/api/task');
$response->assertOk();
}
如果你想要授予所有权限到该令牌,需要在传递给 actingAs
方法的权限列表中包含 *
通配符:
Sanctum::actingAs(
User::factory()->create(),
['*']
);
2 Comments
Class 'App\Models\Authenticatable' not found
我应该继承哪个接口呢?使用 PHPStorm 不是自动引入的吗