[ Laravel 从入门到精通 ] 用户认证与授权系列 —— 通过 Passport 实现 API 请求认证:单页面应用篇

OAuth 与 Passport

前面我们介绍的用户认证都是基于 Web 请求路由的,本质上都是基于 Session 实现的用户认证。在前后端分离大行其道的时代(这里提到的前后端分离包括前端与后端、App与后端、小程序与后端),基于 API 请求的认证也非常常见,但是我们知道常见的 Session 技术都是结合客户端 Cookie 来实现的,从后端剥离出去的前端应用无法通过 API 请求从客户端传递 Cookie 及 CSRF Token 到后端,也就无法通过 Session 实现用户认证。所以,我们需要寻求其它解决方案。

这个解决方案就是 OAuth,OAuth 是一个开发授权标准,允许通过授权的方式让第三方应用访问该用户在某一网站上存储的需要认证的资源,而无需将用户名和密码提供给第三方应用。

在前后端分离的 API 认证中,我们可以把前端应用看作第三方应用,后端应用自然就是这里的网站了,用户认证信息存储在后端网站,当前端需要访问认证资源时,通过后端应用授权的方式访问(授权的前提是前端应用在后端应用注册过,否则不能授权),只有用户允许授权,才可以访问认证资源,否则还是不能访问。当然,我这里是简化了 OAuth 授权逻辑,完整的流程请参考 Laravel 文档

在 Laravel 中实现 API 认证无需手动搭建 OAuth 服务,因为官方为我们提供了 Passport 扩展包,该扩展包基于 OAuth 2.0(OAuth 的下一个版本,关注客户端开发者的简易性,同时为 Web 应用、桌面应用和手机应用提供专门的认证流程),可以帮助我们在几分钟内搭建起完备的 OAuth 服务,实现基于 API 请求的认证。下面我们就来演示如何通过 Passport 快速实现基于 API 请求的认证。

快速上手

安装 & 初始化

首先通过 Composer 安装 Passport 扩展包:

composer require laravel/passport

安装完成后,我们就可以使用 Laravel Passport 提供的一系列脚手架代码快速搭建起 OAuth 服务并实现包含前后端完整功能的 API 请求认证。

进入数据库所在环境,在项目根目录下运行数据库迁移命令创建 OAuth 相关数据表:

php artisan migrate

接下来运行如下 Artisan 命令:

php artisan passport:install

该命令会在 storage 目录下生成 oauth-private.keyoauth-public.key,分别包含 OAuth 服务的私钥和公钥,用于安全令牌的加密解密,然后在 oauth_clients 数据表中初始化两条记录,相当于注册了两个客户端应用,一个用于密码授权令牌认证,一个用于私人访问令牌认证。如果你对这些东西一脸懵逼,没关系,下面我们会陆续讲到,现在你只需要知道这个命令干了啥就行了。

修改模型类

如果要让用户支持 API 认证,需要在对应模型类中使用 Laravel\Passport\HasApiTokens Trait,比如 User 模型类:

...
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    ...

该 Trait 中包含了授权令牌与客户端相关方法,后面我们在认证时会用到。

API 认证路由

Passport 为 API 认证提供了相应的路由,只不过,与之前注册路由不同的是,这一次我们在 AuthServiceProviderboot 方法中注册 API 认证相关路由:

// 顶部引入相应命名空间
use Laravel\Passport\Passport;

...

public function boot()
{
    ...

    // API 认证路由注册
    Passport::routes();
}

默认提供的路由控制器位于 \Laravel\Passport\Http\Controllers 命名空间下,并且路由前缀为 /oauth,如果你想要自定义这些配置,可以在上述 routes 方法中通过第二个参数传入。具体注册的 API 认证相关路由如下:

修改配置文件

最后,修改配置文件 config/auth.php,将 API 认证驱动由 token 修改为 passport

'guards' => [
    ...
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

至此,后端的初始化安装和配置工作就完成了。

单页面应用 API 认证

如果你的 API 认证只用于客户端 JavaScript 与后端接口的交互,比如单页面应用,没必要走复杂的跳转授权流程,可以通过在 App\Http\Kernel$middlewareGroups 中新增一个 CreateFreshApiToken 中间件来实现:

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

该中间件注册在 web 路由中,会在用户首次通过 Web 页面登录表单登录后在 Cookie 中设置一个 Token,这样后续客户端发送请求时就会在 Cookie 中带上这个 Token。在访问需要认证的 API 接口时,会走 auth:api 认证中间件,此时我们的配置文件 config/auth.php 中配置的 API 认证驱动是 passport,在 Laravel 框架底层,根据中间件传入的 api 参数,针对 API 接口认证会通过 Laravel\Passport\Guards\TokenGuard 获取认证信息:

public function user(Request $request)
{
    if ($request->bearerToken()) {
        return $this->authenticateViaBearerToken($request);
    } elseif ($request->cookie(Passport::cookie())) {
        return $this->authenticateViaCookie($request);
    }
}

如果请求头中包含 Bearer Authentication 请求头,则获取对应的请求头 Token 信息,否则从 Cookie 中获取名为 laravel_token 的 Token 信息,由于我们在保存这个 Token 的时候包含了用户ID,所以最终会提取其中的用户 ID 从数据库获取用户数据并返回,从而完整用户认证判断和信息获取。感兴趣的同学可以跟着这个思路去看一下底层的实现代码。

这样,我们就完成后端 API 认证的所有代码编写了,接下来我们来简单测试下这个针对单页面应用的 API 认证。

测试单页面应用 API 认证

Laravel 框架默认在 routes/api.php 中提供了一个认证 API 路由,我们基于这个路由进行测试:

// 为了方便测试,我们先忽略 CSRF 校验
\Laravel\Passport\Passport::$ignoreCsrfToken = true;

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

接下来,我们在浏览器中测试这个 API 接口的访问,由于没有登录,会跳转到登录表单。我们填写登录表单进行登录:

登录完成后,就可以通过 http://blog.test/api/user 获取用户信息了:

F12 查看浏览器开发者工具,在「Network」里追踪请求 Cookie 数据就可以看到这个 laravel_token 信息了:

使用 JavaScript 消费 API

是不是很简单,如果是在 JavaScript 客户端通过 Ajax 异步请求这个认证接口,为了安全起见,需要在请求头中带 X-CSRF-TOKEN,然后去掉 api.php 中的下面这行代码启用 CSRF 验证:

// 为了方便测试,我们先忽略 CSRF 校验
\Laravel\Passport\Passport::$ignoreCsrfToken = true;

如果你使用 Laravel 框架前端资源中集成的 axios 来发起网络请求,那么这个请求头已经设置好了:

let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

你要做的就是在视图的 <head> 标签中添加如下这行代码:

<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">

结语

我们在前后端分离项目 Roast 应用中就使用了这种 API 认证方式,优点是简单易上手,用户体验也比较友好,完全无感知,但是前提是这两个应用可以共享 Cookie,否则需要先获取 Token,然后借助 Bearer Authentication 请求头传递这个 Token。

这只是 Passport 一个最基本的使用,根本还没有涉及到 OAuth 服务,我们借助 Passport 来实现这个功能主要是为了更全面的介绍 Passport 的用法,以及后续教程在此安装配置基础上实现更复杂的授权登录认证,比如第三方应用接入等。如果你目前的场景没有那么复杂,只是单纯实现前后端分离的单页面应用 API 认证,还可以抛开 Passport,基于 Laravel 自带的 API 认证驱动配置 token 来实现,相应实现可以参考这篇教程:基于 Laravel 5.5 构建 & 测试 RESTful API

后面几篇教程我们将基于 Passport 实现更加复杂的授权登录认证。

学院君 has written 1137 articles

Laravel学院院长,终身学习者

积分:144522 等级:P12 职业:手艺人 城市:杭州

10 条回复

  1. 学院君 学院君 says:
    @ Sten

    登录成功后刷新一下又何妨 又不是不能刷新 这是最简化的方案 而且我这里的单页面应用前后端在一个项目里面 没有分离出去 当然你如果不想用Cookie完全可以以其它方式来保存Token 不过就要自己编写额外代码了

  2. Sten Sten says:

    学院君能不能出一期教程讲下SPA如何通过axios发送API请求并通过后端的passport来授权登录/注册功能?(例如重写passport包自带的token生成方法然后以json方式或set-cookie方式返回给前端SPA),另外刚才看到通知说你回复了我上一个评论,可是我点进来却没有看到呀。。

  3. Sten Sten says:

    这里的登录演示的依然是传统的session cookie形式的登录吧?并不是SPA。因为登录成功后页面会刷新,才能成功使用上CreateFreshApiToken生成的laravel_token。如果是前端SPA的axios发送API请求来实现登录认证,CreateFreshApiToken这个Class在页面不刷新的情况下是不生效的。

  4. 王荣 王荣 says:

    我现在的应用都是微信内的,所以我是微信授权成功之后,使用私人访问令牌创建了一个token给前端. 然后之后的请求都用这个token.

    我知道私人访问令牌应该测试的时候用,但是我之前感觉用passport的其他授权页走不通. 今天看了ApiTokenCookieFactory的代码,我感觉可以按照ApiTokenCookieFactory源码里创建token的方式,去创建token使用.

    不知道这个思路好不好?

  5. xiao345 xiao345 says:

    前后端分离不是应该前端后端各一套代码吗?如果前后端各一套代码怎么传CSRF-TOKEN?

登录后才能进行评论,立即登录?