基于 Laravel Sanctum 提供 SPA 认证解决方案


Laravel Sanctum

简介

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 认证信息:

-w904

接下来,如果你是为单页面应用提供认证的话,需要在 app/Http/Kernel.phpapi 中间件分组中添加 Sanctum 中间件:

use Laravel\Airlock\Http\Middleware\EnsureFrontendRequestsAreStateful;
    
'api' => [
    EnsureFrontendRequestsAreStateful::class,
    'throttle:60,1',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

迁移自定义

如果你不准备使用 Sanctum 默认的迁移文件,可以在 AppServiceProviderregister 方法中调用 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(),
    ['*']
);

实例教程


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 基于 Laravel Passport 提供 OAuth2 认证解决方案

>> 下一篇: 基于 Laravel Scout 提供全文搜索解决方案