Passport OAuth 认证

未匹配的标注

Laravel Passport

简介

Laravel Passport 可以在几分钟之内为你的应用程序提供完整的 OAuth2 服务端实现。Passport 是基于由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 server建立的。

注意:本文档假定你已熟悉 OAuth2 。如果你并不了解 OAuth2 ,阅读之前请先熟悉下 OAuth2 的 常用术语 和特性。

选择 Passport 还是 Sanctum ?

在开始之前,我们希望您先确认下是 Laravel Passport 还是 Laravel Sanctum 能为您的应用提供更好的服务。如果您的应用确确实实需要支持OAuth2,那没疑问,你需要选用 Laravel Passport。

然而,如果你只是试图要去认证一个单页应用,或者手机应用,或者发布 API 令牌,您应该选用 Laravel Sanctum。 Laravel Sanctum 不支持OAuth2,它提供了更为简单的 API 授权开发体验。

安装

在开始使用之前,使用 Composer 包管理器安装 Passport:

composer require laravel/passport

Passport的 服务提供器 注册了自己的数据库迁移脚本目录, 所以你应该在安装软件包完成后迁移你自己的数据库。 Passport 的迁移脚本将为你的应用创建用于存储 OAuth2 客户端和访问令牌的数据表:

php artisan migrate

接下来,你需要执行 Artisan 命令 ‘passport:install ’。这个命令将会创建一个用于生成安全访问令牌的加密秘钥。另外,这个命令也将创建用于生成访问令牌的“个人访问”客户端和“密码授权”客户端 :

php artisan passport:install

技巧:如果你想用使用 UUIDS 作为 Passport ‘Client’ 模型的主键,代替默认的自动增长整形字段,请在安装 Passport 时使用 ‘uuids’ 参数

在执行 ‘passport:install’ 命令后, 添加 ‘Laravel\Passport\HasApiTokens’ trait 到你的 ‘App\Models\User’ 模型中。 这个 trait 会提供一些帮助方法用于检查已认证用户的令牌和权限范围:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

接着你需要在 ‘App\Providers\AuthServiceProvider’ 类的 ‘boot’ 方法中调用 ‘Passport::routes’ 方法。这个方法将注册一些必须的路由,用于发布或撤销访问令牌,操作客户端以及个人的访问令牌:

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 应用的策略映射。
     *
     * @var array
     */
    protected $policies = [
        'App\Models\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * 注册鉴权/授权服务。
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        if (! $this->app->routesAreCached()) {
            Passport::routes();
        }
    }
}

最后,在你应用的配置文件 ‘config/auth.php’ 中, 将 api 的授权看守器 guards 的 ‘driver’ 参数的值设置为 ‘passport’。此调整会让你的应用程序使用 Passport 的 ‘TokenGuard’ 鉴权API接口请求:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

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

UUIDS 客户端

你也可以在使用 ‘passport:install’ 命令时带上 ‘--uuids’ 参数。这个参数将促使 Passport 使用 UUIDS 代替默认的自增长整形字段作为 Passport ‘Client’ 模型的主键。 在你带上 ‘--uuids’ 参数执行 ‘passport:install’ 命令后,你将得到关于禁用Passport默认迁移的相关指令说明。

php artisan passport:install --uuids

部署 Passport

当你第一次部署 Passport 到你的应用服务器,你需要运行 ‘passport:keys’ 命令。命令将生成一个Passport需要的加密秘钥,用于生成访问令牌。生成的秘钥不建议放到源码管理中:

php artisan passport:keys

如有必要的话,你需要定义 Passport 秘钥的访问路径。你可以使用 Passport:loadKeysFrom 方法完成这件事。 典型的做法是, 在你应用程序 App\Providers\AuthServiceProvider 类方法 boot 中调用该方法:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}

从环境中加载秘钥

或者你可以使用 Artisan 命令 vendor:publish 发布 Passport 的配置文件:

php artisan vendor:publish --tag=passport-config

配置文件发布好后,可以将加密秘钥定义成环境变量,再加载它们:

PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"

PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"

自定义迁移

如果你不想使用 Passport 的默认迁移,你需要在 App\Providers\AppServiceProvider 类中的 register 方法中调用 Passport::ignoreMigrations 方法。你可以使用 Artisan 命令 vendor:publish导出默认的迁移文件:

php artisan vendor:publish --tag=passport-migrations

Passport 的升级

当升级到Passport新的主要版本时,你一定要仔细查看升级指南

配置

客户端秘钥的 hash 加密

如果你希望客户端秘钥在存储到数据库时被hash加密, 你需要在 App\Providers\AuthServiceProvider 类的 boot 方法中调用 Passport::hashClientSecrets 方法:

use Laravel\Passport\Passport;

Passport::hashClientSecrets();

一旦启用,你的所有客户端秘钥将只有在创建时会显示。由于纯文本的客户秘钥值没有存储在数据库中,所以如果秘钥丢失,也不可能再恢复。

Token 的生命周期

默认情况下,Passport 会发行生命周期长达一年的令牌。 如果那你想要配置更长或者更短周期的秘钥,你可以使用方法 tokensExpireInrefreshTokensExpireIn以及personalAccessTokensExpireIn。 这些方法需要在应用类App\Providers\AuthServiceProviderboot方法中调用:

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::tokensExpireIn(now()->addDays(15));
    Passport::refreshTokensExpireIn(now()->addDays(30));
    Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}

注意:Passport数据库表上的expires_at列是只读的,只用于显示目的。在发行令牌时,Passport将过期信息存储在签名和加密的令牌中。如果你需要使一个令牌失效,你应该撤销它。

重载默认模型

你可以通过定义自己的模型并扩展相应的Passport模型,来自由扩展Passport内部使用的模型:

use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    // ...
}

在自定义模型后,你可以通过Laravel\Passport\Passport类告知 Passport 使用自定义模型。通常情况下,你应该在应用程序App\Providers\AuthServiceProvider类的启动方法中通知 Passport 使用自定义模型:

use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\Token;

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::useTokenModel(Token::class);
    Passport::useClientModel(Client::class);
    Passport::useAuthCodeModel(AuthCode::class);
    Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
}

发布访问令牌

通过授权码使用OAuth2是大多数开发人员熟悉的方式。使用授权码方式时,客户端应用程序会将用户重定向到你的服务器,在那里他们会批准或拒绝向客户端发出访问令牌的请求。

客户端管理

首先,开发者如果想要搭建一个与你的服务端接口交互的应用端,需要在服务端这边注册一个“客户端”。通常,这需要开发者提供应用程序的名称和一个URL,在应用软件的使用者授权请求后,应用程序会被重定向到该URL。

passport:client 命令

使用 Artisan 命令 passport:client 是一种最简单的创建客户端的方式。 这个命令可以创建你自己私有的客户端,用于Oauth2 功能测试。 当你执行 client 命令后, Passport 将会给你更多关于客户端的提示,以及生成的客户端 ID 和秘钥:

php artisan passport:client

多重定向URL地址的设置

如果你想为你的客户端提供多个重定向 URL ,你可以在执行 Passport:client 命令出现提示输入URL地址的时候,输入用逗号分割的多个URL 。任何包含逗号的 URL 都需要先执行 URL 转码:

http://example.com/callback,http://examplefoo.com/callback

JSON API

因为应用程序的开发者是无法使用 client 命令的,所以 Passport 提供了 JSON 格式的 API ,用于创建客户端。 这解决了你还要去手动创建控制器代码(代码用于添加,更新,删除客户端)的麻烦。

你需要结合 Passport 的 JSON API 接口和你的前端面板管理页面, 为你的用户提供客户端管理功能。接下里,我们会回顾所有用于管理客户端的的 API 接口。方便起见,我们使用 Axios模拟对端点的 HTTP 请求。

这些 JSON API 接口被 webauth 两个中间件保护着,因此,你只能从你的应用中调用。 外部来源的调用是被禁止的。

GET /oauth/clients

下面的路由将为授权用户返回所有的客户端。最主要的作用是列出所有的用户客户端,接下来就可以编辑或删除它们了:

axios.get('/oauth/clients')
    .then(response => {
        console.log(response.data);
    });

POST /oauth/clients

下面的路由用于创建新的客户端。 它需要两个参数: 客户端名称重定向URL地址。 重定向URL地址是使用者在授权或者拒绝授权后被重定向到的地方。

客户端被创建后,将会生成客户端 ID 和客户端秘钥。 这对值用于从你的应用获取访问令牌。 调用下面的客户端创建路由将创建新的客户端实例:

const data = {
    name: 'Client Name',
    redirect: 'http://example.com/callback'
};

axios.post('/oauth/clients', data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

PUT /oauth/clients/{client-id}

下面的路由用来更新客户端。它需要两个参数: 客户端名称和重定向URL地址。 重定向URL地址是用户在授权或者拒绝授权后被重定向到的地方。路由将返回更新后的客户端实例:

const data = {
    name: 'New Client Name',
    redirect: 'http://example.com/callback'
};

axios.put('/oauth/clients/' + clientId, data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/clients/{client-id}

下面的路由用于删除客户端:

axios.delete('/oauth/clients/' + clientId)
    .then(response => {
        //
    });

请求令牌

授权重定向

客户端创建好后,开发者使用 client ID 和秘钥向你的应用服务器发送请求,以便获取授权码和访问令牌。 首先,接收到请求的业务端服务器会重定向到你应用的 /oauth/authorize 路由上,如下所示:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
    ]);

    return redirect('http://passport-app.com/oauth/authorize?'.$query);
});

技巧:请记住,/oauth/authorize 路由默认已经在 Passport::routes 方法中定义,你无需手动定义它。

请求认证

当接收到一个请求后, Passport 会自动展示一个模板页面给用户,用户可以选择授权或者拒绝授权。如果请求被认证,用户将被重定向到之前业务服务器设置的重定向地址上去。 这个重定向地址就是客户端在创建时提供的重定向地址参数。

如果你想自定义授权页面,你可以先使用 Artisan 命令 vendor:publish 发布Passport的视图页面。 被发布的视图页面位于 resources/views/vendor/passport 路径下:

php artisan vendor:publish --tag=passport-views

有时候你想跳过授权认证,比如在授权第一梯队客户端的时候。你可以通过继承 Client 模型并实现 skipsAuthorization 方法。如果 skipsAuthorization 方法返回 true, 客户端就会直接被认证并立即重定向到设置的重定向地址:

<?php

namespace App\Models\Passport;

use Laravel\Passport\Client as BaseClient;

class Client extends BaseClient
{
    /**
     * Determine if the client should skip the authorization prompt.
     *
     * @return bool
     */
    public function skipsAuthorization()
    {
        return $this->firstParty();
    }
}

授权码到授权令牌的转化

如果用户授权了访问,他们会被重定向到业务服务端。首先,业务端服务需要检查 state 参数是否和重定向之前存储的值一致。 如果state参数的值正确,业务端服务器需要对你的应用发起获取 access token的 POST 请求。 请求需要携带有授权码,授权码就是之前用户授权后由你的应用服务器生成的码:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class
    );

    $response = Http::asForm()->post('http://passport-app.com/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'code' => $request->code,
    ]);

    return $response->json();
});

调用路由 /oauth/token 将返回一串 json 字符串,包含了 access_token, refresh_tokenexpires_in 属性。expires_in 属性的值是 access_token 剩余的有效时间。

技巧:就和 /oauth/authorize 路由一样, /oauth/token 路由已经在 Passport::routes方法中定义,你无需再自定义这个路由。

JSON API

Passport 同样包含了一个 JSON API 接口用来管理授权访问令牌。你可以使用该接口为用户搭建一个管理访问令牌的控制面板。方便来着,我们将使用 Axios 模拟HTTP对端点发起请求。由于 JSON API 被中间件 webauth 保护着,我们只能在应用内部调用。

GET /oauth/tokens

下面的路由包含了授权用户创建的所有授权访问令牌。接口的主要作用是列出用户所有可撤销的令牌:

axios.get('/oauth/tokens')
    .then(response => {
        console.log(response.data);
    });

DELETE /oauth/tokens/{token-id}

下面的路由用于撤销授权访问令牌以及相关的刷新令牌:

axios.delete('/oauth/tokens/' + tokenId);

刷新令牌

如果你的应用发布的是短生命周期访问令牌,用户需要使用刷新令牌来延长访问令牌的生命周期,刷新令牌是在生成访问令牌时同时生成的:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.com/oauth/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => 'the-refresh-token',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => '',
]);

return $response->json();

调用路由 /oauth/token 将返回一串 json 字符串,包含了 access_token, refresh_tokenexpires_in 属性。expires_in 属性的值是 access_token 剩余的有效时间。

撤销令牌

你可以使用 Laravel\Passport\TokenRepository 类的 revokeAccessToken 方法撤销令牌。你可以使用 Laravel\Passport\RefreshTokenRepository 类的 revokeRefreshTokensByAccessTokenId 方法撤销刷新令牌。这两个类可以通过 Laravel 的服务容器得到:

use Laravel\Passport\TokenRepository;
use Laravel\Passport\RefreshTokenRepository;

$tokenRepository = app(TokenRepository::class);
$refreshTokenRepository = app(RefreshTokenRepository::class);

// Revoke an access token...
$tokenRepository->revokeAccessToken($tokenId);

// Revoke all of the token's refresh tokens...
$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);

清除令牌

如果令牌已经被撤销或者已经过期了,你可能希望把它们从数据库中清理掉。Passport 提供了 Artisan 命令 passport:purge 帮助你实现这个操作:

# 清除已经撤销或者过期的令牌以及授权码
php artisan passport:purge

# 只清理撤销的令牌以及授权码
php artisan passport:purge --revoked

# 只清理过期的令牌以及授权码
php artisan passport:purge --expired

你可以在应用的 App\Console\Kernel 类中配置一个定时任务,每天自动的清理令牌:

/**
 * Define the application's command schedule.
 *
 * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
 * @return void
 */
protected function schedule(Schedule $schedule)
{
    $schedule->command('passport:purge')->hourly();
}

通过 PKCE 发布授权码

通过 PKCE(Proof Key for Code Exchange, 中文译为” 代码交换的证明密钥”) 发放授权码是对单页面应用或原生应用进行认证以便访问 API 接口的安全方式。这种发放授权码是用于不能保证客户端密码被安全储存,或为降低攻击者拦截授权码的威胁。在这种模式下,当授权码获取令牌时,用“验证码”(code verifier) 和“质疑码”(code challenge, “challenge”,名词可译为’挑战;异议;质疑’等)的组合来交换客户端访问密钥。

创建客户端

在使用 PKCE 方式发布令牌之前,你需要先创建一个启用了 PKCE 的客户端。你可以使用 Artisan 命令 passport:client并带上 --public 参数来完成该操作:

php artisan passport:client --public

请求令牌

验证码(Code Verifier )和质疑码(Code Challenge)

这种授权方式不提供授权秘钥,开发者需要创建一个验证码和质疑码的组合来请求得到一个令牌。

验证码是一串包含43位到128位字符的随机字符串。可用字符包括字母,数字以及下面这些字符:"-", ".", "_", "~" 。 可参考RFC 7636 specification定义。

质疑码是一串 Base64 编码包含 URL 和文件名安全字符的字符串,字符串结尾的 = 号需要删除,并且不能包含换行符,空白符或其他附加字符。

$encoded = base64_encode(hash('sha256', $code_verifier, true));

$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');

授权重定向

客户端创建完后,你可以使用客户端ID以及生成的验证码,质疑码从你的应用请求获取授权码和访问令牌。首先,业务端应用需要向服务端路由 /oauth/authorize 发起重定向请求:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $request->session()->put(
        'code_verifier', $code_verifier = Str::random(128)
    );

    $codeChallenge = strtr(rtrim(
        base64_encode(hash('sha256', $code_verifier, true))
    , '='), '+/', '-_');

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
        'code_challenge' => $codeChallenge,
        'code_challenge_method' => 'S256',
    ]);

    return redirect('http://passport-app.com/oauth/authorize?'.$query);
});

验证码到访问令牌的转换

用户授权访问后,将重定向到业务端服务。正如标准授权定义那样,业务端需要验证回传的 state参数的值和在重定向之前设置的值是否一致。

如果 state 的值验证通过,业务接入端需要向应用端发起一个获取访问令牌的 POST 请求。请求的参数需要包括之前用户授权通过后你的应用生成的授权码,以及之前生成的验证码:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    $codeVerifier = $request->session()->pull('code_verifier');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class
    );

    $response = Http::asForm()->post('http://passport-app.com/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'code_verifier' => $codeVerifier,
        'code' => $request->code,
    ]);

    return $response->json();
});

密码授权方式的令牌

OAuth2 的密码授权方式允许你自己的客户端(比如手机端应用),通过使用邮箱/用户名和密码获取访问秘钥。这样你就可以安全的为自己发放令牌,而不需要完整地走 OAuth2 的重定向授权访问流程。

创建密码授权方式客户端

在你使用密码授权方式发布令牌前,你需要先创建密码授权方式的客户端。你可以通过 Artisan 命令 passport:client, 并加上--password 参数来创建这样的客户端。如果你已经运行过 passport:install 命令,则不需要再运行下面的命令

php artisan passport:client --password

请求令牌

密码授权方式的客户端创建好后,你就可以使用用户邮箱和密码向 /oauth/token 路由发起 POST 请求,以获取访问令牌。请记住,该路由已经在 Passport::routes 方法中定义,你无需再手动实现它。如果请求成功,你将在返回 JSON 串中获取到 access_tokenrefresh_token:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.com/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => '',
]);

return $response->json();

技巧:记住,默认情况下 access token 都是长生命周期的,但是如果有需要的话,你可以主动去设置 access token 的过期时间

请求所有的作用域

当使用密码授权(password grant)或者客户端认证授权(client credentials grant)方式时,你可能希望将应用所有的作用域范围都授权给令牌。你可以通过设置 scope 参数为 * 来实现。一旦你这样设置了,所有的 can 方法都将返回 true 值。这种作用域的授权方式只能在密码授权(password grant)或者客户端认证授权(client credentials grant)方式下使用:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.com/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => '*',
]);

自定义用户提供者(User Provider)

如果你的应用使用多个的身份验证用户提供者,你可以通过 --provider 选项来为你的密码授权客户端指定用户提供者。命令 artisan passport:client --password 用于生成密码授权客户端。指定的提供者名称需要是定义在应用配置文件 config/auth.php中的有效用户提供者名称。你可以通过中间件保护你的路由来确保仅授权来自 guard 指定提供者的用户。

自定义用户名字段

当使用密码授权方式验证时,Passport 默认使用你的授权模型的 email 属性作为 “用户名”。当然,你也可以通过在你的模型中定义 findForPassport 方法来定义验证行为:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * Find the user instance for the given username.
     *
     * @param  string  $username
     * @return \App\Models\User
     */
    public function findForPassport($username)
    {
        return $this->where('username', $username)->first();
    }
}

自定义密码验证

当使用密码授权方式时,Passpot 默认使用模型的 password 属性验证密码。如果你的模型没有 password 属性,或者你想自定义密码验证逻辑,你可以在你的模型中自定义方法 validateForPassportPasswordGrant:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * Validate the password of the user for the Passport password grant.
     *
     * @param  string  $password
     * @return bool
     */
    public function validateForPassportPasswordGrant($password)
    {
        return Hash::check($password, $this->password);
    }
}

隐式授权令牌

隐式授权类似于授权码授权;但是,令牌将在不交换授权码的情况下返回给客户端。此授权最常用于无法安全存储客户端凭据的 JavaScript 或移动应用程序。要启用授权,请在应用程序的 App\Providers\AuthServiceProvider 类的 boot 方法中调用 enableImplicitGrant 方法:

/**
 * 注册任何身份验证 / 用户认证服务
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::enableImplicitGrant();
}

启用授权后,开发人员可以使用他们的客户端 ID 从您的应用程序请求访问令牌。接入的应用程序应该向您的应用程序的 /oauth/authorize 路由发出重定向请求,如下所示:

use Illuminate\Http\Request;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://third-party-app.com/callback',
        'response_type' => 'token',
        'scope' => '',
        'state' => $state,
    ]);

    return redirect('http://passport-app.com/oauth/authorize?'.$query);
});

技巧: 请记住,/oauth/authorize 路由已经在 Passport::routes 方法中定义好了,无需手动定义。

客户端凭证授予令牌

客户端凭据授予适用于机器对机器的身份验证。例如,您可以在通过 API 执行维护任务的计划任务中使用此授权。

要想让应用程序可以通过客户端凭据授权发布令牌,首先,您需要创建一个客户端凭据授权客户端。你可以使用 passport:client Artisan 命令的 --client 选项来执行此操作:

php artisan passport:client --client

接下来,要使用这种授权,你首先需要在 app/Http/Kernel.php 的$routeMiddleware 属性中添加 CheckClientCredentials 中间件:

use Laravel\Passport\Http\Middleware\CheckClientCredentials;

protected $routeMiddleware = [
    'client' => CheckClientCredentials::class,
];

之后,在路由上附加中间件:

Route::get('/orders', function (Request $request) {
    ...
})->middleware('client');

要将对路由的访问限制为特定范围,您可以在将 client 中间件附加到路由时提供所需范围的逗号分隔列表:

Route::get('/orders', function (Request $request) {
    ...
})->middleware('client:check-status,your-scope');

获取令牌

要获取此授权类型的令牌,请向 oauth/token 发出请求:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('http://passport-app.com/oauth/token', [
    'grant_type' => 'client_credentials',
    'client_id' => 'client-id',
    'client_secret' => 'client-secret',
    'scope' => 'your-scope',
]);

return $response->json()['access_token'];

个人访问令牌

有时,你的用户要在不经过传统的授权码重定向流程的情况下向自己颁发访问令牌。允许用户通过应用程序用户界面对自己发布令牌,有助于用户体验你的 API,或者也可以将其作为一种更简单的发布访问令牌的方式。

创建个人访问客户端

在你的应用程序发布个人访问令牌之前,你需要在 passport:client 命令后带上 --personal 参数来创建对应的客户端。如果你已经运行了 passport:install 命令,则无需再运行此命令:

php artisan passport:client --personal

创建个人访问客户端后,将客户端的 ID 和纯文本密钥放在应用程序的 .env 文件中:

PASSPORT_PERSONAL_ACCESS_CLIENT_ID="client-id-value"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="unhashed-client-secret-value"

管理个人访问令牌

创建个人访问客户端后,你可以使用 App\Models\User 模型实例上的 createToken 方法来为给定用户发布令牌。 createToken 方法接受令牌的名称作为其第一个参数和可选的 作用域 数组作为其第二个参数

use App\Models\User;

$user = User::find(1);

// 创建没有作用域的令牌...
$token = $user->createToken('Token Name')->accessToken;

// 创建有作用域的令牌...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

JSON API

Passport 中还有一个用于管理个人访问令牌的 JSON API。您可以将其与您自己的前端配对,为您的用户提供一个用于管理个人访问令牌的仪表板。下面,我们将回顾所有用于管理个人访问令牌的 API 。为方便起见,我们将使用 Axios 来演示向 API 发出 HTTP 请求。

JSON API 由 webauth 中间件保护;因此,只能从您自己的应用程序中调用它。无法从外部源调用它。

GET /oauth/scopes

此路由会返回应用中定义的所有 作用域。你可以使用此路由列出用户可以分配给个人访问令牌的范围:

axios.get('/oauth/scopes')
    .then(response => {
        console.log(response.data);
    });

GET /oauth/personal-access-tokens

此路由返回认证用户创建的所有个人访问令牌。这主要用于列出所有用户的令牌,以便他们可以编辑或删除它们:

axios.get('/oauth/personal-access-tokens')
    .then(response => {
        console.log(response.data);
    });

POST /oauth/personal-access-tokens

此路由用于创建新的个人访问令牌。它需要两个数据:令牌的 namescopes

const data = {
    name: 'Token Name',
    scopes: []
};

axios.post('/oauth/personal-access-tokens', data)
    .then(response => {
        console.log(response.data.accessToken);
    })
    .catch (response => {
        // List errors on response...
    });

DELETE /oauth/personal-access-tokens/{token-id}

此路由可用于删除个人访问令牌:

axios.delete('/oauth/personal-access-tokens/' + tokenId);

路由保护

通过中间件

Passport 包含一个 验证保护机制 可以验证请求中传入的访问令牌。 配置 api 的看守器使用 passport 驱动程序后,只需要在需要有效访问令牌的任何路由上指定 auth:api 中间件:

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

多个身份验证 guard

如果您的应用程序可能使用完全不同的 Eloquent 模型、不同类型的用户进行身份验证,则可能需要为应用程序中的每种用户设置 guard。 这使您可以保护特定 guard 的请求。 例如,设置以下 guard config/auth.php 配置文件:

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

'api-customers' => [
    'driver' => 'passport',
    'provider' => 'customers',
],

以下路由将使用 customers 用户提供者的 api-customers guard 来验证传入的请求:

Route::get('/customer', function () {
    //
})->middleware('auth:api-customers');

技巧:有关在 Passport 上使用多个用户提供者的更多信息,请查阅 密码授予文档.

传递访问令牌

当调用 Passport 保护下的路由时,接入的 API 应用需要将访问令牌作为 Bearer 令牌放在请求头 Authorization 中。例如,使用 Guzzle HTTP 库时:

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'Accept' => 'application/json',
    'Authorization' => 'Bearer '.$accessToken,
])->get('https://passport-app.com/api/user');

return $response->json();

令牌作用域

作用域可以让 API 客户端在请求账户授权时请求特定的权限。例如,如果你正在构建电子商务应用程序,并不是所有接入的 API 应用都需要下订单的功能。你可以让接入的 API 应用只被允许授权访问订单发货状态。换句话说,作用域允许应用程序的用户限制第三方应用程序执行的操作。

定义作用域

你可以在 App\Providers\AuthServiceProviderboot 方法中使用 Passport::tokensCan 方法来定义 API 的作用域。tokensCan 方法接受一个包含作用域名称和描述的数组作为参数。作用域描述将会在授权确认页中直接展示给用户,你可以将其定义为任何你需要的内容:

/**
 * 注册身份验证/授权服务。
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::tokensCan([
        'place-orders' => 'Place orders',
        'check-status' => 'Check order status',
    ]);
}

默认作用域

如果客户端没有请求任何特定的范围, 你可以在 App\Providers\AuthServiceProvider 类的 boot 方法中使用 Passport::setDefaultScope 方法来定义默认的作用域。

use Laravel\Passport\Passport;

Passport::tokensCan([
    'place-orders' => 'Place orders',
    'check-status' => 'Check order status',
]);

Passport::setDefaultScope([
    'check-status',
    'place-orders',
]);

给令牌分配作用域

请求授权码

使用授权码请求访问令牌时,接入的应用需为 scope 参数指定所需作用域。 scope 参数包含多个作用域时,名称之间使用空格分割:

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'code',
        'scope' => 'place-orders check-status',
    ]);

    return redirect('http://passport-app.com/oauth/authorize?'.$query);
});

分发个人访问令牌

使用 User 模型的 createToken 方法发放个人访问令牌时,可以将所需作用域的数组作为第二个参数传给此方法:

$token = $user->createToken('My Token', ['place-orders'])->accessToken;

检查作用域

Passport 包含两个中间件,可用于验证传入的请求是否包含访问指定作用域的令牌。 使用之前,需要将下面的中间件添加到 app/Http/Kernel.php 文件的 $routeMiddleware 属性中:

'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,

检查所有作用域

路由可以使用 scopes 中间件来检查当前请求是否拥有指定的 所有 作用域:

Route::get('/orders', function () {
    // 访问令牌具有 "check-status" 和 "place-orders" 作用域...
})->middleware(['auth:api', 'scopes:check-status,place-orders']);

检查任意作用域

路由可以使用 scope 中间件来检查当前请求是否拥有指定的 任意 作用域:

Route::get('/orders', function () {
    // 访问令牌具有 "check-status" 或 "place-orders" 作用域...
})->middleware(['auth:api', 'scope:check-status,place-orders']);

检查令牌实例上的作用域

就算含有访问令牌验证的请求已经通过应用程序的验证,你仍然可以使用当前授权 App\Models\User 实例上的 tokenCan 方法来验证令牌是否拥有指定的作用域:

use Illuminate\Http\Request;

Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('place-orders')) {
        //
    }
});

附加作用域方法

scopeIds 方法将返回所有已定义 ID / 名称的数组:

use Laravel\Passport\Passport;

Passport::scopeIds();

scopes 方法将返回一个包含所有已定义作用域数组的 Laravel\Passport\Scope 实例:

Passport::scopes();

scopesFor 方法将返回与给定 ID / 名称匹配的 Laravel\Passport\Scope 实例数组:

Passport::scopesFor(['place-orders', 'check-status']);

你可以使用 hasScope 方法确定是否已定义给定作用域:

Passport::hasScope('place-orders');

使用 JavaScript 接入 AP

在构建 API 时, 如果能通过 JavaScript 应用接入自己的 API 将会给开发过程带来极大的便利。这种 API 开发方法允许你使用自己的应用程序的 API 和别人共享的 API 。你的 Web 应用程序、移动应用程序、第三方应用程序以及可能在各种软件包管理器上发布的任何 SDK 都可能会使用相同的 API 。

通常,如果要在 JavaScript 应用程序中使用 API ,需要手动向应用程序发送访问令牌,并将其传递给应用程序。但是, Passport 有一个可以处理这个问题的中间件。将 CreateFreshApiToken 中间件添加到 app/Http/Kernel.php 文件中的 web 中间件组就可以了:

'web' => [
    // Other middleware...
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],

注意:你需要确保 CreateFreshApiToken 是你的中间件堆栈中的最后一个中间件。

这个 Passport 中间件将在你所有的对外请求中添加一个 laravel_token cookie 。该 cookie 将包含一个加密后的 JWT , Passport 将用来验证来自 JavaScript 应用程序的 API 请求。JWT 的生命周期等于您的 session.lifetime 配置值。至此,您可以在不明确传递访问令牌的情况下向应用程序的 API 发出请求:

axios.get('/api/user')
    .then(response => {
        console.log(response.data);
    });

自定义 Cookie 名称

如果需要,你可以在 App\Providers\AuthServiceProvider 类的 boot 方法中使用 Passport::cookie 方法来自定义 laravel_token cookie 的名称。

/**
 * 注册认证 / 授权服务
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::cookie('custom_name');
}

CSRF 保护

当使用这种授权方法时,您需要确认请求中包含有效的 CSRF 令牌。默认的 Laravel JavaScript 脚手架会包含一个 Axios 实例,该实例是自动使用加密的 XSRF-TOKEN cookie 值在同源请求上发送 X-XSRF-TOKEN 请求头。

注意:如果您选择发送 X-CSRF-TOKEN 请求头而不是 X-XSRF-TOKEN ,则需要使用 csrf_token() 提供的未加密令牌。

事件

Passport 在发出访问令牌和刷新令牌时引发事件。 您可以使用这些事件来修改或撤消数据库中的其他访问令牌。 您可以在应用程序的 EventServiceProvider 中将监听器附加到这些事件:

/**
 * 应用程序事件监听映射
 *
 * @var array
 */
protected $listen = [
    'Laravel\Passport\Events\AccessTokenCreated' => [
        'App\Listeners\RevokeOldTokens',
    ],

    'Laravel\Passport\Events\RefreshTokenCreated' => [
        'App\Listeners\PruneOldTokens',
    ],
];

测试

Passport 的 actingAs 方法可以指定当前已认证用户及其作用域。 actingAs 方法的第一个参数是用户实例,第二个参数是用户令牌作用域数组:

use App\Models\User;
use Laravel\Passport\Passport;

public function test_servers_can_be_created()
{
    Passport::actingAs(
        User::factory()->create(),
        ['create-servers']
    );

    $response = $this->post('/api/create-server');

    $response->assertStatus(201);
}

Passport 的 actingAsClient 方法可以指定当前已认证用户及其作用域。 actingAsClient 方法的第一个参数是用户实例,第二个参数是用户令牌作用域数组:

use Laravel\Passport\Client;
use Laravel\Passport\Passport;

public function test_orders_can_be_retrieved()
{
    Passport::actingAsClient(
        Client::factory()->create(),
        ['check-status']
    );

    $response = $this->get('/api/orders');

    $response->assertStatus(200);
}

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
贡献者:11
讨论数量: 1
发起讨论 只看当前版本


mengzhi
league/oauth2-server 的authentication code问题
0 个点赞 | 1 个回复 | 问答 | 课程版本 8.5