通过 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.key
和 oauth-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 认证提供了相应的路由,只不过,与之前注册路由不同的是,这一次我们在 AuthServiceProvider
的 boot
方法中注册 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 实现更加复杂的授权登录认证。
15 Comments
CreateFreshApiToken 中间件只在GET方式时会设置 laravel_token, 如果是用ajax POST方式登录的话, 需要手动设置 laravel_token, 方法是在 login controller 中:
我在resources目录下的app.js中加入
resources/views/layouts/app.blade.php中本身存在 <meta name="csrf-token" content="{{ csrf_token() }}"> 为啥开启csrf保护后,登录之后访问api/user 不显示信息,关闭csrf时是正常的.
要是能先讲下这几张表就更好了
对这几张表有什么疑惑吗?
有没有登录页面的代码